killBuildRunner function
Kills any existing build_runner processes before starting a new one.
build_runner uses a lock file (.dart_tool/build/lock) so a second
invocation in the same project will hang indefinitely waiting for the
lock. This function uses two complementary strategies:
Strategy 1 — lock-file owner (most reliable)
Uses lsof -t to find the exact PID(s) holding the lock file and
kills them directly. This bypasses command-line truncation issues that
can cause pkill -f to miss the process (e.g. when Flutter/puro runs
build_runner via a long snapshot path).
Strategy 2 — pkill fallback
pkill -f build_runner catches any remaining dart processes whose
argv contains "build_runner" in case lsof found nothing (e.g. when
the lock file doesn't exist yet but the process is still starting).
Finally, the entire .dart_tool/build directory is deleted so no
stale lock can block the new process, even if the killed process held
the file handle right up to SIGKILL.
Returns the number of processes that were killed (0 = none were running).
Implementation
Future<int> killBuildRunner({String? workingDirectory}) async {
int killed = 0;
if (Platform.isWindows) {
// Windows: use WMIC to find dart.exe processes running build_runner.
try {
final list = await Process.run(
'wmic',
['process', 'where', "CommandLine like '%build_runner%'", 'get', 'ProcessId'],
runInShell: true,
);
final pids = (list.stdout as String)
.split(RegExp(r'\s+'))
.where((s) => RegExp(r'^\d+$').hasMatch(s))
.toList();
for (final pid in pids) {
final r = await Process.run('taskkill', ['/F', '/PID', pid], runInShell: true);
if (r.exitCode == 0) killed++;
}
} catch (_) {}
} else {
// ── Strategy 1: lsof — find the process holding the lock file ───────────
// This is the most reliable method: we know exactly which process is
// blocking. It is immune to argv truncation issues in pkill -f.
if (workingDirectory != null) {
for (final lockName in ['lock', '.lock']) {
final lockPath = p.join(workingDirectory, '.dart_tool', 'build', lockName);
if (!File(lockPath).existsSync()) continue;
try {
final lsof = await Process.run('lsof', ['-t', lockPath]);
final pids = (lsof.stdout as String)
.trim()
.split(RegExp(r'\s+'))
.where((s) => RegExp(r'^\d+$').hasMatch(s))
.toList();
for (final pid in pids) {
await Process.run('kill', ['-TERM', pid]);
killed++;
}
if (pids.isNotEmpty) {
// Grace period for clean shutdown.
await Future.delayed(const Duration(milliseconds: 700));
for (final pid in pids) {
await Process.run('kill', ['-KILL', pid]);
}
}
} catch (_) {}
}
}
// ── Strategy 2: pkill -f as a broad fallback ────────────────────────────
// Catches processes whose lock file doesn't exist yet (still starting up)
// or when lsof is unavailable. On macOS -f searches the full argv.
try {
final r = await Process.run('pkill', ['-f', 'build_runner']);
if (r.exitCode == 0) {
killed++;
await Future.delayed(const Duration(milliseconds: 800));
await Process.run('pkill', ['-9', '-f', 'build_runner']);
}
} catch (_) {}
}
// ── Delete the entire .dart_tool/build directory ─────────────────────────
// Deleting just the lock file is insufficient — the OS may keep the file
// descriptor open briefly after SIGKILL. Removing the whole directory
// guarantees a clean slate. Retry once after a short pause.
if (workingDirectory != null) {
final buildCache = Directory(p.join(workingDirectory, '.dart_tool', 'build'));
if (buildCache.existsSync()) {
await Future.delayed(const Duration(milliseconds: 200));
try {
buildCache.deleteSync(recursive: true);
} catch (_) {
// If the first attempt races with the dying process, wait and retry.
await Future.delayed(const Duration(milliseconds: 400));
try {
buildCache.deleteSync(recursive: true);
} catch (_) {}
}
}
}
return killed;
}