run method

  1. @override
Future<void> run()
override

Runs this command.

The return value is wrapped in a Future if necessary and returned by CommandRunner.runCommand.

Implementation

@override
Future<void> run() async {
  final args = argResults!;
  validateNodeStartArgs(args);
  final hub = args['hub'] as String;
  final id = args['id'] as String;
  final token = args['token'] as String;
  final ca = args['ca'] as String?;
  final context = ca == null
      ? null
      : (SecurityContext(withTrustedRoots: false)
          ..setTrustedCertificates(ca));

  final registry = FormulaRegistry.standard();

  // `node restart` and `node shutdown` act on *this agent*, not on the host,
  // and only the command owning its lifecycle can end it. Completing this is
  // what stops the agent: the exit code then tells a supervisor whether to
  // bring it back — non-zero for a restart, zero for a shutdown, which is
  // what `Restart=on-failure` and Docker's `restart: on-failure` honour.
  final stopped = Completer<int>();
  void stop(int code) {
    if (!stopped.isCompleted) stopped.complete(code);
  }

  final updateService = UpdateService(
    onRestartAgent: () async => stop(agentRestartExitCode),
    onStopAgent: () async => stop(0),
  );
  const monitor = SystemMonitor();
  final scanner = CapabilityScanner.standard();

  // The agent's log goes to this terminal *and*, unless asked not to, to the
  // Hub — so an operator can read what a node is doing without logging into
  // it. The shipper is late-bound because it needs the agent, and the agent's
  // config needs the logger.
  LogShipper? shipper;
  void log(String message) {
    stdout.writeln(message);
    shipper?.add(message);
  }

  // A formula's output goes the same way, so an operator can watch an install
  // happen rather than wait to be told how it went. Until this was wired the
  // lines were produced and dropped: nothing was listening.
  final formulaService = NodeFormulaService(registry: registry, onLog: log);

  final agentConfig = NodeAgentConfig(
    hubUri: Uri.parse(hub),
    nodeId: id,
    credentials: TokenCredentialProvider(
      principal: args['principal'] as String,
      token: token,
    ),
    securityContext: context,
    onBadCertificate: (args['insecure'] as bool)
        ? (cert, host, port) => true
        : null,
    labels: _parseLabels(args['label'] as List<String>, 'label'),
    statusProvider: monitor.snapshot,
    capabilityProvider: scanner.scan,
    formulaHandler: formulaService.runFormula,
    formulaStatusHandler: formulaService.reportStatus,
    presetHandler: formulaService.applyPreset,
    nodeControlHandler: updateService.handle,
    logger: log,
    verbose: args['verbose'] as bool,
  );
  final agent = NodeAgent(agentConfig);
  if (args['ship-logs'] as bool) {
    shipper = LogShipper(send: agent.sendLogs);
  }
  // Say what it is doing before it blocks: start() only returns once the node
  // has registered, so without this a slow — or rejected — connection is a
  // silent hang. The runtime's logger (wired in NodeAgent) then reports any
  // failure and its reason.
  log('Connecting to ${agentConfig.controlUri} …');
  await agent.start();
  log('Node "$id" connected to $hub.');

  // The same machine, also serving shell sessions — one process, one service
  // unit, one supervision target. It is an independent runtime speaking
  // OmnyShell's protocol on the Hub's shell mount; the two share only the
  // credentials and the certificate.
  final shellNode = (args['with-shell'] as bool)
      ? await _startShellNode(
          hubUri: Uri.parse(hub),
          shellPath: args['shell-path'] as String,
          nodeId: id,
          principal: args['principal'] as String,
          token: token,
          securityContext: context,
          insecure: args['insecure'] as bool,
          labels: _parseLabels(
            args['shell-label'] as List<String>,
            'shell-label',
          ),
        )
      : null;

  stdout.writeln('Press Ctrl-C to stop.');
  // Ctrl-C, or the Hub asking this agent to restart or stop. Either way the
  // same orderly shutdown runs; only the exit code differs.
  final code = await Future.any([
    _untilStopped().then((_) => 0),
    stopped.future,
  ]);
  if (code != 0) log('Stopping: the Hub asked this agent to restart.');
  shipper?.close();
  await shellNode?.shutdown();
  await agent.stop();
  // Leave deliberately rather than waiting for the isolate to run dry. A
  // long-running agent holds handles that outlive the work — the signal
  // watcher above among them — and the exit code is the whole contract here:
  // non-zero asks a supervisor for the agent back, zero leaves it stopped.
  _leave(code);
}