Refactor temporary file handling for efficiency (#1865)
diff --git a/bin/dartdoc.dart b/bin/dartdoc.dart
index 903b331..2b70ffa 100644
--- a/bin/dartdoc.dart
+++ b/bin/dartdoc.dart
@@ -11,6 +11,7 @@
import 'package:dartdoc/dartdoc.dart';
import 'package:dartdoc/src/html/html_generator.dart';
import 'package:dartdoc/src/logging.dart';
+import 'package:dartdoc/src/tool_runner.dart';
import 'package:stack_trace/stack_trace.dart';
class DartdocProgramOptionContext extends DartdocGeneratorOptionContext
@@ -94,8 +95,9 @@
}
}, when: config.asyncStackTraces);
} finally {
- // Clear out any cached tool snapshots.
+ // Clear out any cached tool snapshots and temporary directories.
SnapshotCache.instance.dispose();
+ ToolTempFileTracker.instance.dispose();
}
}
diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart
index 2f409fb..15d7a47 100644
--- a/lib/src/dartdoc_options.dart
+++ b/lib/src/dartdoc_options.dart
@@ -21,6 +21,7 @@
import 'package:args/args.dart';
import 'package:dartdoc/dartdoc.dart';
import 'package:dartdoc/src/io_utils.dart';
+import 'package:dartdoc/src/tool_runner.dart';
import 'package:dartdoc/src/tuple.dart';
import 'package:path/path.dart' as pathLib;
import 'package:yaml/yaml.dart';
@@ -243,11 +244,12 @@
return snapshots[toolPath];
}
- void dispose() {
- if (snapshotCache != null && snapshotCache.existsSync()) {
- snapshotCache.deleteSync(recursive: true);
- }
+ Future<void> dispose() {
_instance = null;
+ if (snapshotCache != null && snapshotCache.existsSync()) {
+ return snapshotCache.delete(recursive: true);
+ }
+ return null;
}
}
@@ -288,12 +290,16 @@
class ToolConfiguration {
final Map<String, ToolDefinition> tools;
+ ToolRunner _runner;
+ ToolRunner get runner => _runner ??= ToolRunner(this);
+
ToolConfiguration._(this.tools);
static ToolConfiguration get empty {
return new ToolConfiguration._({});
}
+ // TODO(jcollins-g): consider caching these.
static ToolConfiguration fromYamlMap(
YamlMap yamlMap, pathLib.Context pathContext) {
var newToolDefinitions = <String, ToolDefinition>{};
diff --git a/lib/src/markdown_processor.dart b/lib/src/markdown_processor.dart
index 56b4116..9e00368 100644
--- a/lib/src/markdown_processor.dart
+++ b/lib/src/markdown_processor.dart
@@ -15,7 +15,6 @@
import 'package:dartdoc/src/warnings.dart';
import 'package:html/parser.dart' show parse;
import 'package:markdown/markdown.dart' as md;
-import 'package:quiver/iterables.dart' as quiverIterables;
const validHtmlTags = const [
"a",
diff --git a/lib/src/model.dart b/lib/src/model.dart
index 55262c1..323e58a 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -57,7 +57,6 @@
import 'package:dartdoc/src/model_utils.dart';
import 'package:dartdoc/src/package_meta.dart' show PackageMeta, FileContents;
import 'package:dartdoc/src/special_elements.dart';
-import 'package:dartdoc/src/tool_runner.dart';
import 'package:dartdoc/src/tuple.dart';
import 'package:dartdoc/src/utils.dart';
import 'package:dartdoc/src/warnings.dart';
@@ -4121,9 +4120,6 @@
/// 2018-09-18T21:15+00:00
Future<String> _evaluateTools(String rawDocs) async {
if (config.allowTools) {
- var runner = new ToolRunner(config.tools, (String message) async {
- warn(PackageWarning.toolError, message: message);
- });
int invocationIndex = 0;
return await _replaceAllMappedAsync(rawDocs, basicToolRegExp,
(basicMatch) async {
@@ -4138,7 +4134,8 @@
// Count the number of invocations of tools in this dartdoc block,
// so that tools can differentiate different blocks from each other.
invocationIndex++;
- return await runner.run(args,
+ return await config.tools.runner.run(args,
+ (String message) async => warn(PackageWarning.toolError, message: message),
content: basicMatch[2],
environment: {
'SOURCE_LINE': lineAndColumn?.item1?.toString(),
@@ -4153,7 +4150,7 @@
'ELEMENT_NAME': fullyQualifiedNameWithoutLibrary,
'INVOCATION_INDEX': invocationIndex.toString(),
}..removeWhere((key, value) => value == null));
- }).whenComplete(runner.dispose);
+ });
} else {
return rawDocs;
}
@@ -4783,7 +4780,7 @@
// specialClasses handler so when we find them, they get added.
specialClasses = new SpecialClasses();
// Go through docs of every ModelElement in package to pre-build the macros
- // index.
+ // index. Uses toList() in order to get all the precaching on the stack.
List<Future> precacheFutures = precacheLocalDocs().toList();
for (Future f in precacheFutures) await f;
_localDocumentationBuilt = true;
diff --git a/lib/src/tool_runner.dart b/lib/src/tool_runner.dart
index c31a7ae..58f63c6 100644
--- a/lib/src/tool_runner.dart
+++ b/lib/src/tool_runner.dart
@@ -19,6 +19,35 @@
/// limiting both parallelization and the number of open temporary files.
final MultiFutureTracker _toolTracker = new MultiFutureTracker(4);
+/// Can be called when the ToolRunner is no longer needed.
+///
+/// This will remove any temporary files created by the tool runner.
+class ToolTempFileTracker {
+ final Directory temporaryDirectory;
+
+ ToolTempFileTracker._() : temporaryDirectory = Directory.systemTemp.createTempSync('dartdoc_tools_');
+
+ static ToolTempFileTracker _instance;
+ static ToolTempFileTracker get instance => _instance ??= ToolTempFileTracker._();
+
+ int _temporaryFileCount = 0;
+ Future<File> createTemporaryFile() async {
+ _temporaryFileCount++;
+ File tempFile = new File(pathLib.join(
+ temporaryDirectory.absolute.path,
+ 'input_$_temporaryFileCount'));
+ await tempFile.create(recursive: true);
+ return tempFile;
+ }
+
+ /// Call once no more files are to be created.
+ Future<void> dispose() async {
+ if (temporaryDirectory.existsSync()) {
+ return temporaryDirectory.delete(recursive: true);
+ }
+ }
+}
+
/// A helper class for running external tools.
class ToolRunner {
/// Creates a new ToolRunner.
@@ -26,53 +55,13 @@
/// 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]);
+ ToolRunner(this.toolConfiguration);
final ToolConfiguration toolConfiguration;
- final ToolErrorCallback _errorCallback;
- int _temporaryFileCount = 0;
-
- Future<Directory> _temporaryDirectory;
- Future<Directory> get temporaryDirectory {
- if (_temporaryDirectory == null) {
- _temporaryDirectory = Directory.systemTemp.createTemp('dartdoc_tools_');
- }
- return _temporaryDirectory;
- }
-
- void _error(String message) {
- if (_errorCallback != null) {
- _errorCallback(message);
- }
- }
-
- Future<File> _createTemporaryFile() async {
- _temporaryFileCount++;
- File tempFile = new File(pathLib.join(
- (await temporaryDirectory).absolute.path,
- 'input_$_temporaryFileCount'));
- await tempFile.create(recursive: true);
- return tempFile;
- }
-
- /// 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 != null) disposeAsync(_temporaryDirectory);
- }
-
- /// Avoid blocking on I/O for cleanups.
- static Future<void> disposeAsync(Future<Directory> temporaryDirectory) async {
- Directory tempDir = await temporaryDirectory;
- if (await tempDir.exists()) {
- return tempDir.delete(recursive: true);
- }
- }
void _runSetup(
- String name, ToolDefinition tool, Map<String, String> environment) async {
+ String name, ToolDefinition tool, Map<String, String> environment,
+ ToolErrorCallback toolErrorCallback) async {
bool isDartSetup = ToolDefinition.isDartExecutable(tool.setupCommand[0]);
var args = tool.setupCommand.toList();
String commandPath;
@@ -82,18 +71,19 @@
} else {
commandPath = args.removeAt(0);
}
- await _runProcess(name, '', commandPath, args, environment);
+ await _runProcess(name, '', commandPath, args, environment, toolErrorCallback);
tool.setupComplete = true;
}
Future<String> _runProcess(String name, String content, String commandPath,
- List<String> args, Map<String, String> environment) async {
+ List<String> args, Map<String, String> environment,
+ ToolErrorCallback toolErrorCallback) async {
String commandString() => ([commandPath] + args).join(' ');
try {
ProcessResult result =
await Process.run(commandPath, args, environment: environment);
if (result.exitCode != 0) {
- _error('Tool "$name" returned non-zero exit code '
+ toolErrorCallback('Tool "$name" returned non-zero exit code '
'(${result.exitCode}) when run as '
'"${commandString()}" from ${Directory.current}\n'
'Input to $name was:\n'
@@ -104,7 +94,7 @@
return result.stdout;
}
} on ProcessException catch (exception) {
- _error('Failed to run tool "$name" as '
+ toolErrorCallback('Failed to run tool "$name" as '
'"${commandString()}": $exception\n'
'Input to $name was:\n'
'$content');
@@ -118,18 +108,18 @@
///
/// The [args] must not be null, and it must have at least one member (the name
/// of the tool).
- Future<String> run(List<String> args,
+ Future<String> run(List<String> args, ToolErrorCallback toolErrorCallback,
{String content, Map<String, String> environment}) async {
Future runner;
// Prevent too many tools from running simultaneously.
await _toolTracker.addFutureFromClosure(() {
- runner = _run(args, content: content, environment: environment);
+ runner = _run(args, toolErrorCallback, content: content, environment: environment);
return runner;
});
return runner;
}
- Future<String> _run(List<String> args,
+ Future<String> _run(List<String> args, ToolErrorCallback toolErrorCallback,
{String content, Map<String, String> environment}) async {
assert(args != null);
assert(args.isNotEmpty);
@@ -137,7 +127,7 @@
environment ??= <String, String>{};
var tool = args.removeAt(0);
if (!toolConfiguration.tools.containsKey(tool)) {
- _error('Unable to find definition for tool "$tool" in tool map. '
+ toolErrorCallback('Unable to find definition for tool "$tool" in tool map. '
'Did you add it to dartdoc_options.yaml?');
return '';
}
@@ -152,7 +142,7 @@
// file before running the tool synchronously.
// Write the content to a temp file.
- var tmpFile = await _createTemporaryFile();
+ var tmpFile = await ToolTempFileTracker.instance.createTemporaryFile();
await tmpFile.writeAsString(content);
// Substitute the temp filename for the "$INPUT" token, and all of the other
@@ -188,7 +178,7 @@
}
if (toolDefinition.setupCommand != null && !toolDefinition.setupComplete)
- await _runSetup(tool, toolDefinition, envWithInput);
+ await _runSetup(tool, toolDefinition, envWithInput, toolErrorCallback);
argsWithInput = toolArgs + argsWithInput;
var commandPath;
@@ -201,13 +191,14 @@
} else {
commandPath = argsWithInput.removeAt(0);
}
+
if (callCompleter != null) {
return _runProcess(
- tool, content, commandPath, argsWithInput, envWithInput)
+ tool, content, commandPath, argsWithInput, envWithInput, toolErrorCallback)
.whenComplete(callCompleter);
} else {
return _runProcess(
- tool, content, commandPath, argsWithInput, envWithInput);
+ tool, content, commandPath, argsWithInput, envWithInput, toolErrorCallback);
}
}
}
diff --git a/test/tool_runner_test.dart b/test/tool_runner_test.dart
index 361d3e2..5c9600d 100644
--- a/test/tool_runner_test.dart
+++ b/test/tool_runner_test.dart
@@ -20,6 +20,8 @@
File setupFile;
ToolRunner runner;
+ ToolTempFileTracker tracker;
+ ToolErrorCallback errorCallback;
final errors = <String>[];
setUpAll(() async {
@@ -81,11 +83,12 @@
'missing': new ToolDefinition(['/a/missing/executable'], null, "missing"),
});
- runner = new ToolRunner(toolMap, (String message) => errors.add(message));
+ runner = new ToolRunner(toolMap);
+ errorCallback = (String message) => errors.add(message);
});
tearDownAll(() {
tempDir?.deleteSync(recursive: true);
- runner?.dispose();
+ tracker?.dispose();
SnapshotCache.instance.dispose();
setupFile = null;
tempDir = null;
@@ -100,6 +103,7 @@
test('can invoke a Dart tool, and second run is a snapshot.', () async {
var result = await runner.run(
['drill', r'--file=$INPUT'],
+ errorCallback,
content: 'TEST INPUT',
);
expect(errors, isEmpty);
@@ -109,6 +113,7 @@
expect(setupFile.existsSync(), isFalse);
result = await runner.run(
['drill', r'--file=$INPUT'],
+ errorCallback,
content: 'TEST INPUT 2',
);
expect(errors, isEmpty);
@@ -120,6 +125,7 @@
test('can invoke a Dart tool', () async {
var result = await runner.run(
['drill', r'--file=$INPUT'],
+ errorCallback,
content: 'TEST INPUT',
);
expect(errors, isEmpty);
@@ -131,6 +137,7 @@
test('can invoke a non-Dart tool', () async {
String result = await runner.run(
['non_dart', '--version'],
+ errorCallback,
content: 'TEST INPUT',
);
expect(errors, isEmpty);
@@ -139,6 +146,7 @@
test('can invoke a pre-snapshotted tool', () async {
var result = await runner.run(
['snapshot_drill', r'--file=$INPUT'],
+ errorCallback,
content: 'TEST INPUT',
);
expect(errors, isEmpty);
@@ -148,6 +156,7 @@
test('can invoke a tool with a setup action', () async {
var result = await runner.run(
['setup_drill', r'--file=$INPUT'],
+ errorCallback,
content: 'TEST INPUT',
);
expect(errors, isEmpty);
@@ -158,6 +167,7 @@
test('fails if tool not in tool map', () async {
String result = await runner.run(
['hammer', r'--file=$INPUT'],
+ errorCallback,
content: 'TEST INPUT',
);
expect(errors, isNotEmpty);
@@ -168,6 +178,7 @@
test('fails if tool returns non-zero status', () async {
String result = await runner.run(
['drill', r'--file=/a/missing/file'],
+ errorCallback,
content: 'TEST INPUT',
);
expect(errors, isNotEmpty);
@@ -177,6 +188,7 @@
test("fails if tool in tool map doesn't exist", () async {
String result = await runner.run(
['missing'],
+ errorCallback,
content: 'TEST INPUT',
);
expect(errors, isNotEmpty);