Skip to content

Commit f2aa756

Browse files
Merge pull request #331 from Workiva/background_process_tool
CPLAT-10037: Add a tool to make managing background processes easier.
2 parents 3d75a51 + e9cee45 commit f2aa756

File tree

7 files changed

+163
-6
lines changed

7 files changed

+163
-6
lines changed

doc/tool-composition.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,24 @@ final config = {
140140
};
141141
```
142142

143+
### `BackgroundProcessTool`
144+
145+
The `BackgroundProcessTool` can be used in conjunction with `CompoundTool` to
146+
wrap a tool with the starting and stopping of a background subprocess:
147+
148+
```dart
149+
final testServer = BackgroundProcessTool(
150+
'node', ['tool/server.js'],
151+
delayAfterStart: Duration(seconds: 1));
152+
153+
final config = {
154+
'test': CompoundTool()
155+
..addTool(testServer.starter, alwaysRun: true)
156+
..addTool(TestTool())
157+
..addTool(testServer.stopper, alwaysRun: true),
158+
};
159+
```
160+
143161
### Mapping args to tools
144162

145163
`CompoundTool.addTool()` supports an optional `argMapper` parameter that can be

lib/dart_dev.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export 'src/tools/compound_tool.dart'
66
show ArgMapper, CompoundTool, CompoundToolMixin, takeAllArgs;
77
export 'src/tools/format_tool.dart'
88
show FormatMode, Formatter, FormatterInputs, FormatTool;
9-
export 'src/tools/process_tool.dart' show ProcessTool;
9+
export 'src/tools/process_tool.dart' show BackgroundProcessTool, ProcessTool;
1010
export 'src/tools/test_tool.dart' show TestTool;
1111
export 'src/tools/tuneup_check_tool.dart' show TuneupCheckTool;
1212
export 'src/tools/webdev_serve_tool.dart' show WebdevServeTool;

lib/src/tools/process_tool.dart

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import 'dart:async';
22
import 'dart:io';
33

4-
import 'package:dart_dev/src/utils/start_process_and_ensure_exit.dart';
4+
import 'package:io/io.dart';
55
import 'package:logging/logging.dart';
6+
import 'package:pedantic/pedantic.dart';
67

78
import '../dart_dev_tool.dart';
89
import '../utils/assert_no_positional_args_nor_args_after_separator.dart';
10+
import '../utils/ensure_process_exit.dart';
911
import '../utils/logging.dart';
1012
import '../utils/process_declaration.dart';
13+
import '../utils/start_process_and_ensure_exit.dart';
1114

1215
final _log = Logger('Process');
1316

@@ -56,3 +59,72 @@ class ProcessTool extends DevTool {
5659
return _process.exitCode;
5760
}
5861
}
62+
63+
class BackgroundProcessTool {
64+
final List<String> _args;
65+
final String _executable;
66+
final ProcessStartMode _mode;
67+
final Duration _delayAfterStart;
68+
final String _workingDirectory;
69+
70+
BackgroundProcessTool(String executable, List<String> args,
71+
{ProcessStartMode mode,
72+
Duration delayAfterStart,
73+
String workingDirectory})
74+
: _args = args,
75+
_executable = executable,
76+
_mode = mode,
77+
_delayAfterStart = delayAfterStart,
78+
_workingDirectory = workingDirectory;
79+
80+
Process get process => _process;
81+
Process _process;
82+
83+
DevTool get starter => DevTool.fromFunction(_start);
84+
85+
DevTool get stopper => DevTool.fromFunction(_stop);
86+
87+
bool _processHasExited = false;
88+
89+
Future<int> _start(DevToolExecutionContext context) async {
90+
if (context.argResults != null) {
91+
assertNoPositionalArgsNorArgsAfterSeparator(
92+
context.argResults, context.usageException,
93+
commandName: context.commandName);
94+
}
95+
logSubprocessHeader(_log, '$_executable ${_args.join(' ')}');
96+
97+
final mode = _mode ??
98+
(context.verbose
99+
? ProcessStartMode.inheritStdio
100+
: ProcessStartMode.normal);
101+
_process = await Process.start(_executable, _args,
102+
mode: mode, workingDirectory: _workingDirectory);
103+
ensureProcessExit(_process);
104+
unawaited(_process.exitCode.then((_) => _processHasExited = true));
105+
106+
if (_delayAfterStart != null) {
107+
await Future<void>.delayed(_delayAfterStart);
108+
}
109+
110+
if (_processHasExited) {
111+
// If the background process exits immediately or before the start delay,
112+
// something is probably wrong, so return that exit code.
113+
return _process.exitCode;
114+
}
115+
116+
return ExitCode.success.code;
117+
}
118+
119+
Future<int> _stop(DevToolExecutionContext context) async {
120+
if (context.argResults != null) {
121+
assertNoPositionalArgsNorArgsAfterSeparator(
122+
context.argResults, context.usageException,
123+
commandName: context.commandName);
124+
}
125+
_log.info('Stopping: $_executable ${_args.join(' ')}');
126+
_process?.kill();
127+
await _process.exitCode;
128+
return ExitCode.success.code;
129+
}
130+
}

lib/utils.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
export 'src/utils/assert_no_positional_args_nor_args_after_separator.dart';
22
export 'src/utils/cached_pubspec.dart';
3+
export 'src/utils/ensure_process_exit.dart';
4+
export 'src/utils/global_package_is_active_and_compatible.dart';
35
export 'src/utils/logging.dart'
46
show humanReadable, logSubprocessHeader, logTimedAsync, logTimedSync;
5-
export 'src/utils/global_package_is_active_and_compatible.dart';
67
export 'src/utils/package_is_immediate_dependency.dart';
78
export 'src/utils/process_declaration.dart';
89
export 'src/utils/run_process_and_ensure_exit.dart';

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies:
1515
io: ^0.3.3
1616
logging: ^0.11.3+2
1717
path: ^1.6.2
18+
pedantic: ^1.7.0
1819
pub_semver: ^1.4.2
1920
pubspec_parse: ^0.1.4
2021
stack_trace: ^1.9.3
@@ -23,7 +24,6 @@ dependencies:
2324
dev_dependencies:
2425
dependency_validator: ^1.3.0
2526
matcher: ^0.12.5
26-
pedantic: ^1.7.0
2727
test: ^1.6.4
2828
test_descriptor: ^1.2.0
2929
test_process: ^1.0.4

test/functional.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ Future<TestProcess> runDevToolFunctionalTest(
5555
}
5656
}
5757

58-
final args = <String>['run', 'dart_dev', ...command.split(' ')];
59-
return TestProcess.start('pub', args, workingDirectory: d.sandbox);
58+
final allArgs = <String>['run', 'dart_dev', command, ...?args];
59+
return TestProcess.start('pub', allArgs, workingDirectory: d.sandbox);
6060
}

test/tools/process_tool_test.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:args/args.dart';
55
import 'package:args/command_runner.dart';
66
import 'package:dart_dev/dart_dev.dart';
77
import 'package:logging/logging.dart';
8+
import 'package:pedantic/pedantic.dart';
89
import 'package:test/test.dart';
910

1011
import '../log_matchers.dart';
@@ -42,4 +43,69 @@ void main() {
4243
DevTool.fromProcess('true', ['foo', 'bar']).run();
4344
});
4445
});
46+
47+
group('BackgroundProcessTool', () {
48+
sharedDevToolTests(() => BackgroundProcessTool('true', []).starter);
49+
sharedDevToolTests(() => BackgroundProcessTool('true', []).stopper);
50+
51+
test('starter runs the process without waiting for it to complete',
52+
() async {
53+
var processHasExited = false;
54+
final tool = BackgroundProcessTool('sleep', ['5']);
55+
expect(await tool.starter.run(), isZero);
56+
unawaited(tool.process.exitCode.then((_) => processHasExited = true));
57+
await Future<void>.delayed(Duration.zero);
58+
expect(processHasExited, isFalse);
59+
await tool.stopper.run();
60+
});
61+
62+
test('stopper stops the process immediately', () async {
63+
var processHasExited = false;
64+
final tool = BackgroundProcessTool('sleep', ['5']);
65+
final stopwatch = Stopwatch()..start();
66+
expect(await tool.starter.run(), isZero);
67+
unawaited(tool.process.exitCode.then((_) => processHasExited = true));
68+
await Future<void>.delayed(Duration(seconds: 1));
69+
expect(processHasExited, isFalse);
70+
expect(await tool.stopper.run(), isZero);
71+
expect(processHasExited, isTrue);
72+
expect((stopwatch..stop()).elapsed.inSeconds, lessThan(3));
73+
});
74+
75+
test('starter forwards the returned exit code', () async {
76+
final tool = BackgroundProcessTool('false', [],
77+
delayAfterStart: Duration(milliseconds: 500));
78+
expect(await tool.starter.run(), isNonZero);
79+
});
80+
81+
test('stopper always returns a zero exit code', () async {
82+
final tool = BackgroundProcessTool('false', []);
83+
await tool.starter.run();
84+
await Future<void>.delayed(Duration(milliseconds: 500));
85+
expect(await tool.stopper.run(), isZero);
86+
});
87+
88+
test('can run from a custom working directory', () async {
89+
final tool = BackgroundProcessTool('pwd', [],
90+
workingDirectory: 'lib', delayAfterStart: Duration(seconds: 1));
91+
expect(await tool.starter.run(), isZero);
92+
final stdout =
93+
(await tool.process.stdout.transform(utf8.decoder).join('')).trim();
94+
expect(stdout, endsWith('/dart_dev/lib'));
95+
});
96+
97+
test('throws UsageException when args are present', () {
98+
final tool = BackgroundProcessTool('true', []);
99+
expect(
100+
() => tool.starter.run(
101+
DevToolExecutionContext(argResults: ArgParser().parse(['foo']))),
102+
throwsA(isA<UsageException>()));
103+
});
104+
105+
test('logs the subprocess', () {
106+
expect(Logger.root.onRecord,
107+
emitsThrough(infoLogOf(contains('true foo bar'))));
108+
BackgroundProcessTool('true', ['foo', 'bar']).starter.run();
109+
});
110+
});
45111
}

0 commit comments

Comments
 (0)