[dart_data_home] Add PID files helpers
createPidFile(dir, content) can be used to create the pid file for the current process with the given content in the dir. listPidFiles(dir) can be used to list pid files in the given directory. Both functions will purge stale pid files from the given directory. The implementation operates under the assumption that there is only a small number of processes using pid files running at any given time so there will ever be only a very small number of pid files in the directory. TEST=pid_files_test Change-Id: Id83a7f6138511f755ab47347c17b44d66a6a6964 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/487000 Reviewed-by: Martin Kustermann <kustermann@google.com> Reviewed-by: Jaime Wren <jwren@google.com>
This commit is contained in:
@@ -3,11 +3,16 @@
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
/// @docImport 'src/dart_data_home.dart';
|
||||
/// @docImport 'src/pid_files.dart';
|
||||
|
||||
/// A package providing [getDartDataHome], a standardized way to access a
|
||||
/// user-specific data directory for Dart and Flutter tooling, defaulting to
|
||||
/// OS-conventions and configurable via the `DART_DATA_HOME` environment
|
||||
/// variable.
|
||||
///
|
||||
/// This package also provides utilities [createPidFile] and [listPidFiles]
|
||||
/// for managing pid files for service processes.
|
||||
library;
|
||||
|
||||
export 'src/dart_data_home.dart';
|
||||
export 'src/pid_files.dart';
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) 2026, 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.
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// Create a pid file for the current process in the [directory] with [content].
|
||||
///
|
||||
/// This function will additionally purge any stale pid files in the
|
||||
/// [directory].
|
||||
///
|
||||
/// Returns `true` if pid file was successfully created.
|
||||
///
|
||||
/// Throws an error if a pid file for the current process have been already
|
||||
/// created.
|
||||
bool createPidFile(String directory, String content) {
|
||||
if (_pidFile != null) {
|
||||
throw StateError('Already created pid file for the current process');
|
||||
}
|
||||
try {
|
||||
final dataDir = Directory(directory);
|
||||
dataDir.createSync(recursive: true);
|
||||
final pidFile = File(p.join(dataDir.path, '$pid'));
|
||||
// It is extremely unlikely that we retry this more than couple of times,
|
||||
// but it is possible to hit this case if another process is constantly
|
||||
// calling _purgeStalePidFiles which consistently deletes the pid file we
|
||||
// are creating.
|
||||
for (var attempt = 0; attempt < 10 && _pidFile == null; attempt++) {
|
||||
RandomAccessFile? raf;
|
||||
try {
|
||||
raf = pidFile.openSync(mode: FileMode.writeOnly);
|
||||
// On Windows it is enough to keep the file open to prevent its deletion
|
||||
// by another process, so we do not need to lock it. On POSIX systems
|
||||
// we use advisory file locks instead to synchronize between one process
|
||||
// creating a pid-file and another process trying to check if the
|
||||
// pid-file is stale or not (see _purgeStalePidFiles below).
|
||||
if (!Platform.isWindows) {
|
||||
raf.lockSync();
|
||||
}
|
||||
raf
|
||||
..writeStringSync(content)
|
||||
..flushSync();
|
||||
// On Windows we are good to go: keep the file open so that
|
||||
// _purgeStalePidFiles can detect that the file is not stale.
|
||||
//
|
||||
// On POSIX we need to check if open followed by lock has raced
|
||||
// with another process deleting a stale pid-file: another process
|
||||
// might have opened - locked - deleted - unlocked the file after
|
||||
// we opened it but before we locked it. This way we end up with a
|
||||
// file descriptor corresponding to a deleted file. Check that
|
||||
// the path still exists - if it does not, retry pid file creation.
|
||||
//
|
||||
// Caveat: we must never open pid file corresponding to the current
|
||||
// process because closing it again will release all file locks
|
||||
// acquired by the current process and break the logic in
|
||||
// _purgeStalePidFiles - even though we have another file descriptor
|
||||
// for the same file still open in the current process.
|
||||
if (Platform.isWindows || pidFile.existsSync()) {
|
||||
// We have successfully created a pid file for the current process.
|
||||
// Keep the file open to keep the lock on the file so that
|
||||
// _purgeStalePidFiles can detect that the file is not stale.
|
||||
_pidFile = raf;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
} finally {
|
||||
// We lost the race: We created file but before we could lock it another
|
||||
// process deleted it.
|
||||
//
|
||||
// We have to close our fd, and try again in next loop iteration.
|
||||
if (raf != _pidFile) {
|
||||
raf?.closeSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
_purgeStalePidFiles(dataDir);
|
||||
} catch (_) {}
|
||||
|
||||
return _pidFile != null;
|
||||
}
|
||||
|
||||
/// Loads content of all pid files in the given [directory] excluding the pid
|
||||
/// file of the current process.
|
||||
///
|
||||
/// Returns a map where keys are pids and values are content of the pid file.
|
||||
///
|
||||
/// This function will purge all stale pid files in the [directory].
|
||||
Map<int, String> listPidFiles(String directory) {
|
||||
final currentPid = pid;
|
||||
final result = <int, String>{};
|
||||
try {
|
||||
final dataDir = Directory(directory);
|
||||
if (dataDir.existsSync()) {
|
||||
_purgeStalePidFiles(dataDir);
|
||||
for (final file in dataDir.listSync().whereType<File>()) {
|
||||
final pid = int.tryParse(p.basename(file.path));
|
||||
if (pid == null || pid == currentPid) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// Caveat: on POSIX systems must never read the pid file
|
||||
// corresponding to the current process because reading it will
|
||||
// open and close it - and closing it will release all file
|
||||
// locks associated with this file held by the current process even
|
||||
// if they were acquired through other still open file descriptors
|
||||
// and this will break the logic _purgeStalePidFiles.
|
||||
//
|
||||
// It's okay to read pid files of other processes though as this
|
||||
// will not affect locks held by other processes.
|
||||
assert(pid != currentPid);
|
||||
result[pid] = file.readAsStringSync();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return result;
|
||||
}
|
||||
|
||||
RandomAccessFile? _pidFile;
|
||||
|
||||
/// Purge all stale pid files in the given [dir] except for the one
|
||||
/// corresponding to the current process.
|
||||
void _purgeStalePidFiles(Directory dir) {
|
||||
final currentPid = pid;
|
||||
for (final file in dir.listSync().whereType<File>()) {
|
||||
final pid = int.tryParse(p.basename(file.path));
|
||||
if (pid == null || pid == currentPid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RandomAccessFile? raf;
|
||||
try {
|
||||
// On Windows we rely on the fact that deleting a file can only succeed
|
||||
// if no process has it open. The original process which created pid
|
||||
// file will keep it open for write - so trying to delete it will fail
|
||||
// as long as the process is alive.
|
||||
//
|
||||
// On POSIX we use file locks to detect if PID file is stale or not:
|
||||
// original owner keeps the file locked until it exits. This means
|
||||
// that if we manage to lock the file - the owner has exited.
|
||||
if (!Platform.isWindows) {
|
||||
raf = file.openSync(mode: FileMode.writeOnlyAppend);
|
||||
raf.lockSync();
|
||||
}
|
||||
file.deleteSync();
|
||||
} catch (_) {
|
||||
// Ignore any exceptions.
|
||||
} finally {
|
||||
raf?.closeSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,11 @@ resolution: workspace
|
||||
|
||||
dependencies:
|
||||
cli_util: 0.5.0-wip
|
||||
path: ^1.9.0
|
||||
|
||||
# We use 'any' version constraints here as we get our package versions from
|
||||
# the dart-lang/sdk repo's DEPS file. Note that this is a special case; the
|
||||
# best practice for packages is to specify their compatible version ranges.
|
||||
# See also https://dart.dev/tools/pub/dependencies.
|
||||
dev_dependencies:
|
||||
path: any
|
||||
test: any
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2026, 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.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dart_data_home/src/pid_files.dart';
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
if (args.length < 2) {
|
||||
print('Usage: test_script.dart <package_name> <pid_file_content>');
|
||||
exit(1);
|
||||
}
|
||||
if (!createPidFile(args[0], args[1])) {
|
||||
throw StateError('Failed to create pid file');
|
||||
}
|
||||
print('OK:$pid');
|
||||
|
||||
while (true) {
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
print('.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2026, 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.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:dart_data_home/dart_data_home.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const verboseSubprocesses = false;
|
||||
|
||||
final packageRoot = p.dirname(
|
||||
p.dirname(
|
||||
Isolate.resolvePackageUriSync(
|
||||
Uri.parse('package:dart_data_home/dart_data_home.dart'),
|
||||
)!.toFilePath(),
|
||||
),
|
||||
);
|
||||
|
||||
final testsDir = p.join(packageRoot, 'test');
|
||||
|
||||
// Note: on Windows in JIT mode we have dart.exe spawning dartvm.exe, and
|
||||
// underlying pid files will be created using the pid of the dartvm.exe while
|
||||
// process.pid gives us access to the process id of the dart.exe. To accomodate
|
||||
// for this in tests we send underlying PID from child process to the parent
|
||||
// over stdout.
|
||||
typedef TestScriptProcess = ({Process process, int pid});
|
||||
|
||||
Future<TestScriptProcess> startTestScript(
|
||||
String processName,
|
||||
String packageName,
|
||||
String pidFileContent,
|
||||
) async {
|
||||
final scriptPath = p.join(testsDir, 'common', 'test_script.dart');
|
||||
final process = await Process.start(Platform.resolvedExecutable, [
|
||||
scriptPath,
|
||||
packageName,
|
||||
pidFileContent,
|
||||
]);
|
||||
|
||||
final ready = Completer<int>();
|
||||
|
||||
process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(
|
||||
(line) {
|
||||
if (verboseSubprocesses) {
|
||||
print('[$processName] $line');
|
||||
}
|
||||
if (line.startsWith('OK:')) {
|
||||
ready.complete(int.parse(line.split(':')[1]));
|
||||
}
|
||||
},
|
||||
);
|
||||
process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(
|
||||
(line) {
|
||||
if (verboseSubprocesses) {
|
||||
print('[$processName] $line');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return (process: process, pid: await ready.future);
|
||||
}
|
||||
|
||||
Future<void> killProcess(TestScriptProcess p) async {
|
||||
p.process.kill();
|
||||
await p.process.exitCode;
|
||||
if (Platform.isWindows) {
|
||||
// On Windows in JIT mode we have dart.exe spawning dartvm.exe. Which
|
||||
// means dart.exe exiting does not imply that dartvm.exe has already
|
||||
// terminated as well. We don't have Dart API to wait for a specific
|
||||
// process by PID so just give it a second to terminate.
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
|
||||
setUp(() {
|
||||
tempDir = Directory.systemTemp.createTempSync();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
try {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
} catch (_) {
|
||||
// Ignore any exceptions.
|
||||
}
|
||||
});
|
||||
|
||||
test('pid files', () async {
|
||||
({Process process, int pid})? p1;
|
||||
({Process process, int pid})? p2;
|
||||
|
||||
try {
|
||||
p1 = await startTestScript(
|
||||
'p1',
|
||||
tempDir.path,
|
||||
'test_content_from_script_p1',
|
||||
);
|
||||
p2 = await startTestScript(
|
||||
'p2',
|
||||
tempDir.path,
|
||||
'test_content_from_script_p2',
|
||||
);
|
||||
|
||||
// Both processes should be found by listPidFiles.
|
||||
expect(
|
||||
listPidFiles(tempDir.path),
|
||||
equals({
|
||||
p1.pid: 'test_content_from_script_p1',
|
||||
p2.pid: 'test_content_from_script_p2',
|
||||
}),
|
||||
);
|
||||
|
||||
// Kill one process and check that only one process is now found.
|
||||
await killProcess(p1);
|
||||
expect(
|
||||
listPidFiles(tempDir.path),
|
||||
equals({p2.pid: 'test_content_from_script_p2'}),
|
||||
);
|
||||
|
||||
// Kill the second process and check that no processes are found.
|
||||
await killProcess(p2);
|
||||
expect(listPidFiles(tempDir.path).isEmpty, isTrue);
|
||||
} finally {
|
||||
p1?.process.kill();
|
||||
p2?.process.kill();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user