blob: aec543312198ad53028430d1cfbfceb73382dc3d [file]
// 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();
}
}
}