blob: c55ff4a450f50e586c19687e1d09d86bf039005a [file] [log] [blame]
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library dartdoc.tool_runner;
import 'dart:io';
import 'package:path/path.dart' as pathLib;
import 'dartdoc_options.dart';
typedef ToolErrorCallback = void Function(String message);
typedef FakeResultCallback = String Function(String tool,
{List<String> args, String content});
/// A helper class for running external tools.
class ToolRunner {
/// Creates a new ToolRunner.
///
/// Takes a [toolConfiguration] that describes all of the available tools.
/// An optional `errorCallback` will be called for each error message
/// generated by the tool.
ToolRunner(this.toolConfiguration, [this._errorCallback])
: temporaryDirectory =
Directory.systemTemp.createTempSync('dartdoc_tools_');
final ToolConfiguration toolConfiguration;
final Directory temporaryDirectory;
final ToolErrorCallback _errorCallback;
int _temporaryFileCount = 0;
void _error(String message) {
if (_errorCallback != null) {
_errorCallback(message);
}
}
File _createTemporaryFile() {
_temporaryFileCount++;
return new File(pathLib.join(
temporaryDirectory.absolute.path, 'input_$_temporaryFileCount'))
..createSync(recursive: true);
}
/// Must be called when the ToolRunner is no longer needed. Ideally,
/// this is called in the finally section of a try/finally.
///
/// This will remove any temporary files created by the tool runner.
void dispose() {
if (temporaryDirectory.existsSync())
temporaryDirectory.deleteSync(recursive: true);
}
/// Run a tool. The name of the tool is the first argument in the [args].
/// The content to be sent to to the tool is given in the optional [content],
/// and the stdout of the tool is returned.
///
/// The [args] must not be null, and it must have at least one member (the name
/// of the tool).
String run(List<String> args,
{String content, Map<String, String> environment}) {
assert(args != null);
assert(args.isNotEmpty);
content ??= '';
environment ??= <String, String>{};
var tool = args.removeAt(0);
if (!toolConfiguration.tools.containsKey(tool)) {
_error('Unable to find definition for tool "$tool" in tool map. '
'Did you add it to dartdoc_options.yaml?');
return '';
}
var toolDefinition = toolConfiguration.tools[tool];
var toolArgs = toolDefinition.command;
if (pathLib.extension(toolDefinition.command.first) == '.dart') {
// For dart tools, we want to invoke them with Dart.
toolArgs.insert(0, Platform.resolvedExecutable);
}
// Ideally, we would just be able to send the input text into stdin,
// but there's no way to do that synchronously, and converting dartdoc
// to an async model of execution is a huge amount of work. Using
// dart:cli's waitFor feels like a hack (and requires a similar amount
// of work anyhow to fix order of execution issues). So, instead, we
// have the tool take a filename as part of its arguments, and write
// the input to a temporary file before running the tool synchronously.
// Write the content to a temp file.
var tmpFile = _createTemporaryFile();
tmpFile.writeAsStringSync(content);
// Substitute the temp filename for the "$INPUT" token, and all of the
// other environment variables.
// Variables are allowed to either be in $(VAR) form, or $VAR form.
var envWithInput = {'INPUT': tmpFile.absolute.path}..addAll(environment);
var substitutions = envWithInput.map<RegExp, String>((key, value) {
String escapedKey = RegExp.escape(key);
return MapEntry(RegExp('\\\$(\\($escapedKey\\)|$escapedKey\\b)'), value);
});
var argsWithInput = <String>[];
for (var arg in args) {
var newArg = arg;
substitutions
.forEach((regex, value) => newArg = newArg.replaceAll(regex, value));
argsWithInput.add(newArg);
}
argsWithInput = toolArgs + argsWithInput;
final commandPath = argsWithInput.removeAt(0);
String commandString() => ([commandPath] + argsWithInput).join(' ');
try {
var result = Process.runSync(commandPath, argsWithInput,
environment: envWithInput);
if (result.exitCode != 0) {
_error('Tool "$tool" returned non-zero exit code '
'(${result.exitCode}) when run as '
'"${commandString()}".\n'
'Input to $tool was:\n'
'$content\n'
'Stderr output was:\n${result.stderr}\n');
return '';
} else {
return result.stdout;
}
} on ProcessException catch (exception) {
_error('Failed to run tool "$tool" as '
'"${commandString()}": $exception\n'
'Input to $tool was:\n'
'$content');
return '';
}
}
}