runSupervised function

Future<void> runSupervised(
  1. List<String> arguments
)

Runs the worker in supervised mode.

Launches the same binary as a child process (without --supervised) and monitors its exit code:

  • exit 0: normal shutdown, supervisor exits too.
  • exit 42: update requested, restart the worker.
  • other: crash, wait 10 seconds and restart.

With pub.dev-based updates, dart pub global activate is already executed by the auto_updater before exiting with code 42. The supervisor only needs to restart the process.

Implementation

Future<void> runSupervised(List<String> arguments) async {
  final workerArgs = arguments.where((a) => a != '--supervised').toList();

  // Detect if running via pub global (Platform.resolvedExecutable points to
  // the dart binary, not a compiled AOT executable).
  final resolvedExe = Platform.resolvedExecutable;
  final isRunningViaPubGlobal =
      resolvedExe.endsWith('/dart') || resolvedExe.endsWith(r'\dart.exe');

  while (true) {
    _log.info('Starting worker process...');

    final String executable;
    final List<String> processArgs;

    final installedAot = _findInstalledAotBinary();
    if (installedAot != null) {
      _log.info('Found installed AOT binary: $installedAot');
      executable = installedAot;
      processArgs = workerArgs;
    } else if (isRunningViaPubGlobal) {
      // Use `dart pub global run` to re-enter through the package entrypoint.
      executable = resolvedExe;
      processArgs = [
        'pub',
        'global',
        'run',
        'openci_worker_cli',
        ...workerArgs,
      ];
    } else {
      // AOT-compiled binary: just re-run self.
      executable = resolvedExe;
      processArgs = workerArgs;
    }

    Process process;
    StreamSubscription? sigtermSub;
    StreamSubscription? sigintSub;

    try {
      process = await Process.start(
        executable,
        processArgs,
        mode: ProcessStartMode.inheritStdio,
      );

      // Forward system signals (SIGTERM / SIGINT) to child process to ensure graceful shutdown
      // and prevent child process from becoming a zombie when supervisor is terminated.
      if (!Platform.isWindows) {
        sigtermSub = ProcessSignal.sigterm.watch().listen((sig) {
          _log.info(
            'Supervisor received SIGTERM. Forwarding to child process...',
          );
          process.kill(ProcessSignal.sigterm);
        });
        sigintSub = ProcessSignal.sigint.watch().listen((sig) {
          _log.info(
            'Supervisor received SIGINT. Forwarding to child process...',
          );
          process.kill(ProcessSignal.sigint);
        });
      }
    } on ProcessException catch (e, s) {
      _log.warning(
        'Failed to start worker process (binary may be temporarily missing or locked): $e. '
        'Restarting in 10 seconds...',
      );
      await Sentry.captureException(e, stackTrace: s);
      await Future<void>.delayed(const Duration(seconds: 10));
      continue;
    }

    final exitCode = await process.exitCode;
    await sigtermSub?.cancel();
    await sigintSub?.cancel();

    switch (exitCode) {
      case 0:
        _log.info('Worker exited normally.');
        return;

      case exitCodeUpdateRequested:
        _log.info('Update installed. Restarting worker...');

      default:
        _log.warning(
          'Worker crashed (exit code $exitCode). Restarting in 10 seconds...',
        );
        await Future<void>.delayed(const Duration(seconds: 10));
    }
  }
}