run method
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);
// Blueprints run over the same formulas, through a provider. The ledger goes
// on disk, under the node's own home: an in-memory one would forget what the
// node owns on every restart, and a node that has forgotten what it owns can
// never really have a blueprint unassigned from it — it would adopt
// everything it found and remove nothing.
final blueprintService = NodeBlueprintService(
providers: ProviderRegistry.of([FormulaProvider(registry: registry)]),
ledgers: FileLedgerStore(OmnyServerHome.ensure().path),
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,
blueprintPlanHandler: blueprintService.plan,
blueprintApplyHandler: blueprintService.apply,
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);
}