@onklave/agent-cli 0.1.11 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main.js +550 -3
- package/package.json +1 -1
package/main.js
CHANGED
|
@@ -604,8 +604,8 @@ var PlatformClient = class {
|
|
|
604
604
|
/*
|
|
605
605
|
* Make an authenticated HTTP request to the platform.
|
|
606
606
|
*/
|
|
607
|
-
async request(method,
|
|
608
|
-
const url = `${this.baseUrl}${
|
|
607
|
+
async request(method, path7, body) {
|
|
608
|
+
const url = `${this.baseUrl}${path7}`;
|
|
609
609
|
const headers = {
|
|
610
610
|
Authorization: `Bearer ${this.token}`,
|
|
611
611
|
"Content-Type": "application/json",
|
|
@@ -908,6 +908,20 @@ var SessionManager = class {
|
|
|
908
908
|
}
|
|
909
909
|
};
|
|
910
910
|
|
|
911
|
+
// _apps/@onklave/agent-cli/src/types/events.types.ts
|
|
912
|
+
var DAEMON_AUDIT_ACTIONS = {
|
|
913
|
+
STARTED: "daemon.started",
|
|
914
|
+
ONLINE: "daemon.online",
|
|
915
|
+
DRAINING: "daemon.draining",
|
|
916
|
+
STOPPED: "daemon.stopped",
|
|
917
|
+
FORCE_STOPPED: "daemon.force_stopped",
|
|
918
|
+
DRAIN_TIMEOUT_EXCEEDED: "daemon.drain_timeout_exceeded",
|
|
919
|
+
AUTH_WARNING: "daemon.auth_warning",
|
|
920
|
+
AUTH_EXPIRED: "daemon.auth_expired",
|
|
921
|
+
HEARTBEAT_FAILED: "daemon.heartbeat_failed",
|
|
922
|
+
RESOURCE_LIMIT_BREACHED: "daemon.resource_limit_breached"
|
|
923
|
+
};
|
|
924
|
+
|
|
911
925
|
// _apps/@onklave/agent-cli/src/services/state-publisher.ts
|
|
912
926
|
var StatePublisher = class {
|
|
913
927
|
constructor(commsClient) {
|
|
@@ -2846,6 +2860,538 @@ async function logsCommand(args) {
|
|
|
2846
2860
|
}
|
|
2847
2861
|
}
|
|
2848
2862
|
|
|
2863
|
+
// _apps/@onklave/agent-cli/src/commands/daemon.command.ts
|
|
2864
|
+
import * as fs6 from "fs";
|
|
2865
|
+
|
|
2866
|
+
// _apps/@onklave/agent-cli/src/services/daemon-comms.service.ts
|
|
2867
|
+
var DaemonCommsService = class {
|
|
2868
|
+
constructor(opts, client) {
|
|
2869
|
+
this.disconnectedAt = null;
|
|
2870
|
+
this.stopRequested = false;
|
|
2871
|
+
this.client = client ?? new CommsClient();
|
|
2872
|
+
this.opts = opts;
|
|
2873
|
+
this.sleep = opts.sleep ?? defaultSleep;
|
|
2874
|
+
this.onRetry = opts.onRetry ?? ((attempt, delayMs, err) => console.warn(
|
|
2875
|
+
`[daemon-comms] reconnect attempt ${attempt} after ${delayMs}ms (${err.message})`
|
|
2876
|
+
));
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Opens the socket connection. Retries indefinitely with jittered
|
|
2880
|
+
* exponential backoff until either (a) a connect succeeds or (b)
|
|
2881
|
+
* `stop()` is called. Throws only if `stop()` is invoked before any
|
|
2882
|
+
* successful connect.
|
|
2883
|
+
*/
|
|
2884
|
+
async connect() {
|
|
2885
|
+
let attempt = 0;
|
|
2886
|
+
while (!this.stopRequested) {
|
|
2887
|
+
try {
|
|
2888
|
+
await this.client.connect(this.opts.platformUrl, this.opts.token, {
|
|
2889
|
+
machineId: this.opts.machineId,
|
|
2890
|
+
deviceToken: this.opts.deviceToken,
|
|
2891
|
+
orgId: this.opts.orgId
|
|
2892
|
+
});
|
|
2893
|
+
this.disconnectedAt = null;
|
|
2894
|
+
return;
|
|
2895
|
+
} catch (err) {
|
|
2896
|
+
attempt += 1;
|
|
2897
|
+
const delay = this.computeBackoff(attempt);
|
|
2898
|
+
this.onRetry(attempt, delay, err);
|
|
2899
|
+
if (this.disconnectedAt == null) this.disconnectedAt = /* @__PURE__ */ new Date();
|
|
2900
|
+
await this.sleep(delay);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
throw new Error("daemon-comms: stop() called before any connect succeeded");
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Signal that no further reconnect should be attempted and close the
|
|
2907
|
+
* underlying socket cleanly. Idempotent.
|
|
2908
|
+
*/
|
|
2909
|
+
disconnect() {
|
|
2910
|
+
this.stopRequested = true;
|
|
2911
|
+
this.client.disconnect();
|
|
2912
|
+
}
|
|
2913
|
+
inner() {
|
|
2914
|
+
return this.client;
|
|
2915
|
+
}
|
|
2916
|
+
isConnected() {
|
|
2917
|
+
return this.client.isConnected();
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Continuous-disconnect duration in ms (null if currently connected).
|
|
2921
|
+
* Per spec §3.3, when this exceeds 120s the daemon's `status` output
|
|
2922
|
+
* should report `offline` — the platform will have done so via
|
|
2923
|
+
* heartbeat staleness anyway.
|
|
2924
|
+
*/
|
|
2925
|
+
disconnectedForMs() {
|
|
2926
|
+
if (!this.disconnectedAt) return null;
|
|
2927
|
+
return Date.now() - this.disconnectedAt.getTime();
|
|
2928
|
+
}
|
|
2929
|
+
computeBackoff(attempt) {
|
|
2930
|
+
const max = this.opts.maxBackoffMs ?? 6e4;
|
|
2931
|
+
const base = Math.min(max, 1e3 * 2 ** (attempt - 1));
|
|
2932
|
+
const j = this.opts.jitter ?? 0.2;
|
|
2933
|
+
const swing = base * j;
|
|
2934
|
+
return Math.round(base + (Math.random() * 2 - 1) * swing);
|
|
2935
|
+
}
|
|
2936
|
+
};
|
|
2937
|
+
function defaultSleep(ms) {
|
|
2938
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
// _apps/@onklave/agent-cli/src/services/daemon-state.service.ts
|
|
2942
|
+
import * as fs5 from "fs";
|
|
2943
|
+
import * as path6 from "path";
|
|
2944
|
+
import * as os4 from "os";
|
|
2945
|
+
var VALID_TRANSITIONS = {
|
|
2946
|
+
installing: ["registered"],
|
|
2947
|
+
registered: ["starting"],
|
|
2948
|
+
starting: ["online", "stopping"],
|
|
2949
|
+
online: ["draining", "stopping"],
|
|
2950
|
+
draining: ["stopping"],
|
|
2951
|
+
stopping: ["stopped"],
|
|
2952
|
+
stopped: ["starting"]
|
|
2953
|
+
};
|
|
2954
|
+
function defaultStateFilePath() {
|
|
2955
|
+
return path6.join(os4.homedir(), ".config", "onklave", "daemon.state.json");
|
|
2956
|
+
}
|
|
2957
|
+
function defaultPidFilePath() {
|
|
2958
|
+
return path6.join(os4.homedir(), ".config", "onklave", "daemon.pid");
|
|
2959
|
+
}
|
|
2960
|
+
var DaemonStateError = class extends Error {
|
|
2961
|
+
constructor(message) {
|
|
2962
|
+
super(message);
|
|
2963
|
+
this.name = "DaemonStateError";
|
|
2964
|
+
}
|
|
2965
|
+
};
|
|
2966
|
+
var DaemonStateService = class {
|
|
2967
|
+
constructor(stateFile = defaultStateFilePath(), initial = "registered", machineId) {
|
|
2968
|
+
this.stateFile = stateFile;
|
|
2969
|
+
this.machineId = machineId;
|
|
2970
|
+
this.listeners = [];
|
|
2971
|
+
this.current = initial;
|
|
2972
|
+
this.enteredAt = /* @__PURE__ */ new Date();
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Read the persisted state from disk, if any. Used by `daemon status`
|
|
2976
|
+
* when the daemon process is not running. Returns null on missing or
|
|
2977
|
+
* malformed file rather than throwing so the caller can decide.
|
|
2978
|
+
*/
|
|
2979
|
+
static readPersisted(stateFile = defaultStateFilePath()) {
|
|
2980
|
+
try {
|
|
2981
|
+
if (!fs5.existsSync(stateFile)) return null;
|
|
2982
|
+
const raw = fs5.readFileSync(stateFile, "utf8");
|
|
2983
|
+
const parsed = JSON.parse(raw);
|
|
2984
|
+
if (!parsed.state || !parsed.enteredAt) return null;
|
|
2985
|
+
return parsed;
|
|
2986
|
+
} catch {
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
getCurrent() {
|
|
2991
|
+
return this.current;
|
|
2992
|
+
}
|
|
2993
|
+
getEnteredAt() {
|
|
2994
|
+
return this.enteredAt;
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Subscribe to transitions. Listeners are awaited sequentially in
|
|
2998
|
+
* registration order. If a listener throws, the transition is treated
|
|
2999
|
+
* as failed — the state is rolled back and the error propagates so the
|
|
3000
|
+
* caller can decide. (Audit emission listeners that fail must therefore
|
|
3001
|
+
* fail the transition — that is the constitutional `audit-fails-action`
|
|
3002
|
+
* coupling described in spec §1.4.)
|
|
3003
|
+
*/
|
|
3004
|
+
onTransition(cb) {
|
|
3005
|
+
this.listeners.push(cb);
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Attempt a transition. Throws DaemonStateError on illegal transition
|
|
3009
|
+
* (current → next not in VALID_TRANSITIONS). Throws whatever a listener
|
|
3010
|
+
* throws if any listener fails; in that case state is rolled back to
|
|
3011
|
+
* `prev` and is not persisted.
|
|
3012
|
+
*/
|
|
3013
|
+
async transition(next, opts = {}) {
|
|
3014
|
+
const prev = this.current;
|
|
3015
|
+
const allowed = VALID_TRANSITIONS[prev];
|
|
3016
|
+
if (!allowed.includes(next)) {
|
|
3017
|
+
throw new DaemonStateError(`illegal daemon transition ${prev} \u2192 ${next}`);
|
|
3018
|
+
}
|
|
3019
|
+
this.current = next;
|
|
3020
|
+
this.enteredAt = /* @__PURE__ */ new Date();
|
|
3021
|
+
try {
|
|
3022
|
+
for (const listener of this.listeners) {
|
|
3023
|
+
await listener(next, prev, opts.reason);
|
|
3024
|
+
}
|
|
3025
|
+
this.persist(opts.reason);
|
|
3026
|
+
} catch (err) {
|
|
3027
|
+
this.current = prev;
|
|
3028
|
+
throw err;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
persist(reason) {
|
|
3032
|
+
const dir = path6.dirname(this.stateFile);
|
|
3033
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
3034
|
+
const payload = {
|
|
3035
|
+
state: this.current,
|
|
3036
|
+
enteredAt: this.enteredAt.toISOString(),
|
|
3037
|
+
pid: process.pid,
|
|
3038
|
+
...this.machineId ? { machineId: this.machineId } : {},
|
|
3039
|
+
...reason ? { reason } : {}
|
|
3040
|
+
};
|
|
3041
|
+
const tmp = `${this.stateFile}.tmp`;
|
|
3042
|
+
fs5.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
3043
|
+
fs5.renameSync(tmp, this.stateFile);
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Remove the persisted state file. Called on terminal `stopped` once
|
|
3047
|
+
* the daemon process is exiting cleanly so `daemon status` reports
|
|
3048
|
+
* "registered — daemon not running" instead of stale "stopped".
|
|
3049
|
+
*/
|
|
3050
|
+
clearPersisted() {
|
|
3051
|
+
try {
|
|
3052
|
+
fs5.unlinkSync(this.stateFile);
|
|
3053
|
+
} catch {
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
|
|
3058
|
+
// _apps/@onklave/agent-cli/src/services/unit-file-emitters.ts
|
|
3059
|
+
var SYSTEMD_UNIT = `[Unit]
|
|
3060
|
+
Description=Onklave Agent CLI Daemon
|
|
3061
|
+
Documentation=https://docs.onklave.app/cli/daemon
|
|
3062
|
+
After=network-online.target
|
|
3063
|
+
Wants=network-online.target
|
|
3064
|
+
|
|
3065
|
+
[Service]
|
|
3066
|
+
Type=simple
|
|
3067
|
+
User=onklave
|
|
3068
|
+
Group=onklave
|
|
3069
|
+
Environment="NODE_ENV=production"
|
|
3070
|
+
Environment="ONKLAVE_CONFIG_DIR=/var/lib/onklave"
|
|
3071
|
+
Environment="ONKLAVE_LOG_DIR=/var/log/onklave"
|
|
3072
|
+
ExecStart=/usr/local/bin/onklave daemon
|
|
3073
|
+
ExecStop=/usr/local/bin/onklave daemon drain --timeout 1800
|
|
3074
|
+
Restart=on-failure
|
|
3075
|
+
RestartSec=10s
|
|
3076
|
+
StartLimitIntervalSec=300
|
|
3077
|
+
StartLimitBurst=5
|
|
3078
|
+
KillMode=mixed
|
|
3079
|
+
KillSignal=SIGTERM
|
|
3080
|
+
TimeoutStopSec=1830s
|
|
3081
|
+
|
|
3082
|
+
# Hardening
|
|
3083
|
+
NoNewPrivileges=true
|
|
3084
|
+
ProtectSystem=strict
|
|
3085
|
+
ProtectHome=true
|
|
3086
|
+
PrivateTmp=true
|
|
3087
|
+
ReadWritePaths=/var/lib/onklave /var/log/onklave
|
|
3088
|
+
|
|
3089
|
+
[Install]
|
|
3090
|
+
WantedBy=multi-user.target
|
|
3091
|
+
`;
|
|
3092
|
+
var LAUNCHD_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3093
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3094
|
+
<plist version="1.0">
|
|
3095
|
+
<dict>
|
|
3096
|
+
<key>Label</key>
|
|
3097
|
+
<string>app.onklave.daemon</string>
|
|
3098
|
+
<key>ProgramArguments</key>
|
|
3099
|
+
<array>
|
|
3100
|
+
<string>/usr/local/bin/onklave</string>
|
|
3101
|
+
<string>daemon</string>
|
|
3102
|
+
</array>
|
|
3103
|
+
<key>RunAtLoad</key>
|
|
3104
|
+
<true/>
|
|
3105
|
+
<key>KeepAlive</key>
|
|
3106
|
+
<dict>
|
|
3107
|
+
<key>SuccessfulExit</key>
|
|
3108
|
+
<false/>
|
|
3109
|
+
<key>Crashed</key>
|
|
3110
|
+
<true/>
|
|
3111
|
+
</dict>
|
|
3112
|
+
<key>ThrottleInterval</key>
|
|
3113
|
+
<integer>10</integer>
|
|
3114
|
+
<key>EnvironmentVariables</key>
|
|
3115
|
+
<dict>
|
|
3116
|
+
<key>NODE_ENV</key>
|
|
3117
|
+
<string>production</string>
|
|
3118
|
+
<key>ONKLAVE_CONFIG_DIR</key>
|
|
3119
|
+
<string>/Users/Shared/onklave</string>
|
|
3120
|
+
</dict>
|
|
3121
|
+
<key>StandardOutPath</key>
|
|
3122
|
+
<string>/Users/Shared/onklave/logs/daemon.out.log</string>
|
|
3123
|
+
<key>StandardErrorPath</key>
|
|
3124
|
+
<string>/Users/Shared/onklave/logs/daemon.err.log</string>
|
|
3125
|
+
<key>ProcessType</key>
|
|
3126
|
+
<string>Background</string>
|
|
3127
|
+
</dict>
|
|
3128
|
+
</plist>
|
|
3129
|
+
`;
|
|
3130
|
+
var NSSM_SCRIPT = `@echo off
|
|
3131
|
+
REM Onklave Agent CLI \u2014 NSSM service install script
|
|
3132
|
+
REM Run this as Administrator.
|
|
3133
|
+
|
|
3134
|
+
set NSSM=nssm.exe
|
|
3135
|
+
set SERVICE=OnklaveDaemon
|
|
3136
|
+
set NODE_EXE=%ProgramFiles%\\nodejs\\node.exe
|
|
3137
|
+
set ONKLAVE_CLI=%AppData%\\npm\\node_modules\\@onklave\\agent-cli\\dist\\bin\\onklave.js
|
|
3138
|
+
|
|
3139
|
+
%NSSM% install %SERVICE% "%NODE_EXE%" "%ONKLAVE_CLI%" daemon
|
|
3140
|
+
%NSSM% set %SERVICE% DisplayName "Onklave Agent CLI Daemon"
|
|
3141
|
+
%NSSM% set %SERVICE% Description "Long-running agent worker for the Onklave platform"
|
|
3142
|
+
%NSSM% set %SERVICE% Start SERVICE_AUTO_START
|
|
3143
|
+
%NSSM% set %SERVICE% AppEnvironmentExtra NODE_ENV=production ONKLAVE_CONFIG_DIR=%ProgramData%\\Onklave
|
|
3144
|
+
%NSSM% set %SERVICE% AppStdout %ProgramData%\\Onklave\\logs\\daemon.out.log
|
|
3145
|
+
%NSSM% set %SERVICE% AppStderr %ProgramData%\\Onklave\\logs\\daemon.err.log
|
|
3146
|
+
%NSSM% set %SERVICE% AppRotateFiles 1
|
|
3147
|
+
%NSSM% set %SERVICE% AppRotateOnline 1
|
|
3148
|
+
%NSSM% set %SERVICE% AppRotateBytes 10485760
|
|
3149
|
+
%NSSM% set %SERVICE% AppExit Default Restart
|
|
3150
|
+
%NSSM% set %SERVICE% AppRestartDelay 10000
|
|
3151
|
+
|
|
3152
|
+
%NSSM% start %SERVICE%
|
|
3153
|
+
`;
|
|
3154
|
+
function emitUnitFile(flavour) {
|
|
3155
|
+
switch (flavour) {
|
|
3156
|
+
case "systemd":
|
|
3157
|
+
return SYSTEMD_UNIT;
|
|
3158
|
+
case "launchd":
|
|
3159
|
+
return LAUNCHD_PLIST;
|
|
3160
|
+
case "nssm":
|
|
3161
|
+
return NSSM_SCRIPT;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
// _apps/@onklave/agent-cli/src/commands/daemon.command.ts
|
|
3166
|
+
var UNIT_FLAGS = {
|
|
3167
|
+
"--emit-systemd-unit": "systemd",
|
|
3168
|
+
"--emit-launchd-plist": "launchd",
|
|
3169
|
+
"--emit-nssm-script": "nssm"
|
|
3170
|
+
};
|
|
3171
|
+
async function daemonCommand(args) {
|
|
3172
|
+
for (const arg of args) {
|
|
3173
|
+
const flavour = UNIT_FLAGS[arg];
|
|
3174
|
+
if (flavour) {
|
|
3175
|
+
process.stdout.write(emitUnitFile(flavour));
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
const [sub] = args;
|
|
3180
|
+
if (sub === "status") return daemonStatus();
|
|
3181
|
+
if (sub === "stop" || sub === "drain") return daemonStop();
|
|
3182
|
+
if (sub && !sub.startsWith("--")) {
|
|
3183
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
3184
|
+
console.error(
|
|
3185
|
+
"Usage: onklave daemon [status|stop|drain] | --emit-systemd-unit | --emit-launchd-plist | --emit-nssm-script"
|
|
3186
|
+
);
|
|
3187
|
+
process.exitCode = 1;
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
return daemonStart();
|
|
3191
|
+
}
|
|
3192
|
+
async function daemonStart() {
|
|
3193
|
+
const authService = new AuthService();
|
|
3194
|
+
const creds = await authService.getCredentials();
|
|
3195
|
+
if (!creds?.deviceToken || !creds?.machineId) {
|
|
3196
|
+
console.error(
|
|
3197
|
+
"Error: machine is not registered. Run: onklave register --token <bootstrap>"
|
|
3198
|
+
);
|
|
3199
|
+
process.exitCode = 1;
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
const pidFile = defaultPidFilePath();
|
|
3203
|
+
if (isPidAlive(pidFile)) {
|
|
3204
|
+
console.error(`Daemon already running (pid ${readPid(pidFile)}).`);
|
|
3205
|
+
process.exitCode = 1;
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
const stateService = new DaemonStateService(
|
|
3209
|
+
defaultStateFilePath(),
|
|
3210
|
+
"registered",
|
|
3211
|
+
creds.machineId
|
|
3212
|
+
);
|
|
3213
|
+
const platformUrl = await authService.getPlatformUrl();
|
|
3214
|
+
const comms = new DaemonCommsService({
|
|
3215
|
+
platformUrl,
|
|
3216
|
+
token: creds.token,
|
|
3217
|
+
machineId: creds.machineId,
|
|
3218
|
+
deviceToken: creds.deviceToken,
|
|
3219
|
+
orgId: creds.orgId
|
|
3220
|
+
});
|
|
3221
|
+
const auditStreamer = new AuditStreamer(comms.inner());
|
|
3222
|
+
stateService.onTransition(async (next, prev, reason) => {
|
|
3223
|
+
const action = transitionToAction(next);
|
|
3224
|
+
if (!action) return;
|
|
3225
|
+
const event = {
|
|
3226
|
+
sessionId: creds.machineId,
|
|
3227
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3228
|
+
action,
|
|
3229
|
+
details: { from: prev, to: next, ...reason ? { reason } : {} },
|
|
3230
|
+
outcome: next === "stopped" && reason ? "failure" : "success"
|
|
3231
|
+
};
|
|
3232
|
+
auditStreamer.record(event);
|
|
3233
|
+
});
|
|
3234
|
+
let activeSessions = 0;
|
|
3235
|
+
let shuttingDown = false;
|
|
3236
|
+
let sigintCount = 0;
|
|
3237
|
+
let sigintTimer = null;
|
|
3238
|
+
const heartbeat = new HeartbeatService({
|
|
3239
|
+
platformUrl,
|
|
3240
|
+
deviceToken: creds.deviceToken,
|
|
3241
|
+
machineId: creds.machineId,
|
|
3242
|
+
getActiveSessionCount: () => activeSessions
|
|
3243
|
+
});
|
|
3244
|
+
const drainAndExit = async (reason) => {
|
|
3245
|
+
if (shuttingDown) return;
|
|
3246
|
+
shuttingDown = true;
|
|
3247
|
+
heartbeat.stop();
|
|
3248
|
+
try {
|
|
3249
|
+
if (stateService.getCurrent() === "online") {
|
|
3250
|
+
await stateService.transition("draining", { reason });
|
|
3251
|
+
}
|
|
3252
|
+
while (activeSessions > 0) {
|
|
3253
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
3254
|
+
}
|
|
3255
|
+
await stateService.transition("stopping", { reason });
|
|
3256
|
+
auditStreamer.flush();
|
|
3257
|
+
comms.disconnect();
|
|
3258
|
+
await stateService.transition("stopped");
|
|
3259
|
+
} finally {
|
|
3260
|
+
removePid(pidFile);
|
|
3261
|
+
stateService.clearPersisted();
|
|
3262
|
+
process.exit(0);
|
|
3263
|
+
}
|
|
3264
|
+
};
|
|
3265
|
+
process.on("SIGTERM", () => {
|
|
3266
|
+
void drainAndExit("SIGTERM");
|
|
3267
|
+
});
|
|
3268
|
+
process.on("SIGINT", () => {
|
|
3269
|
+
sigintCount += 1;
|
|
3270
|
+
if (sigintCount >= 2) {
|
|
3271
|
+
console.error("\nForce-stop requested. Exiting immediately.");
|
|
3272
|
+
removePid(pidFile);
|
|
3273
|
+
process.exit(1);
|
|
3274
|
+
}
|
|
3275
|
+
if (!sigintTimer) {
|
|
3276
|
+
sigintTimer = setTimeout(() => {
|
|
3277
|
+
sigintCount = 0;
|
|
3278
|
+
sigintTimer = null;
|
|
3279
|
+
}, 5e3);
|
|
3280
|
+
sigintTimer.unref?.();
|
|
3281
|
+
}
|
|
3282
|
+
console.log(
|
|
3283
|
+
"\nSIGINT received. Draining (Ctrl-C again within 5s to force stop)\u2026"
|
|
3284
|
+
);
|
|
3285
|
+
void drainAndExit("SIGINT");
|
|
3286
|
+
});
|
|
3287
|
+
await stateService.transition("starting");
|
|
3288
|
+
writePid(pidFile);
|
|
3289
|
+
console.log("Connecting to Onklave comms service...");
|
|
3290
|
+
try {
|
|
3291
|
+
await comms.connect();
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
console.error(
|
|
3294
|
+
`Comms connect aborted: ${err.message}. Shutting down.`
|
|
3295
|
+
);
|
|
3296
|
+
removePid(pidFile);
|
|
3297
|
+
stateService.clearPersisted();
|
|
3298
|
+
process.exit(1);
|
|
3299
|
+
}
|
|
3300
|
+
console.log("Connected.");
|
|
3301
|
+
await stateService.transition("online");
|
|
3302
|
+
heartbeat.start();
|
|
3303
|
+
console.log(
|
|
3304
|
+
`Daemon online. machineId=${creds.machineId} pid=${process.pid}. Press Ctrl-C to drain.`
|
|
3305
|
+
);
|
|
3306
|
+
await new Promise(() => void 0);
|
|
3307
|
+
}
|
|
3308
|
+
async function daemonStatus() {
|
|
3309
|
+
const persisted = DaemonStateService.readPersisted();
|
|
3310
|
+
if (!persisted) {
|
|
3311
|
+
console.log("registered \u2014 daemon not running");
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
const alive = persisted.pid ? isProcessAlive(persisted.pid) : false;
|
|
3315
|
+
if (!alive) {
|
|
3316
|
+
console.log(
|
|
3317
|
+
`${persisted.state} \u2014 daemon process not running (last entered ${persisted.enteredAt})`
|
|
3318
|
+
);
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
console.log(`State: ${persisted.state}`);
|
|
3322
|
+
if (persisted.machineId)
|
|
3323
|
+
console.log(`Machine ID: ${persisted.machineId}`);
|
|
3324
|
+
if (persisted.pid) console.log(`PID: ${persisted.pid}`);
|
|
3325
|
+
console.log(`Entered state: ${persisted.enteredAt}`);
|
|
3326
|
+
if (persisted.reason) console.log(`Reason: ${persisted.reason}`);
|
|
3327
|
+
}
|
|
3328
|
+
async function daemonStop() {
|
|
3329
|
+
const pidFile = defaultPidFilePath();
|
|
3330
|
+
const pid = readPid(pidFile);
|
|
3331
|
+
if (!pid || !isProcessAlive(pid)) {
|
|
3332
|
+
console.log("No daemon running.");
|
|
3333
|
+
removePid(pidFile);
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
try {
|
|
3337
|
+
process.kill(pid, "SIGTERM");
|
|
3338
|
+
console.log(`Sent SIGTERM to daemon (pid ${pid}).`);
|
|
3339
|
+
} catch (err) {
|
|
3340
|
+
console.error(`Failed to signal daemon: ${err.message}`);
|
|
3341
|
+
process.exitCode = 1;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
function transitionToAction(next) {
|
|
3345
|
+
switch (next) {
|
|
3346
|
+
case "starting":
|
|
3347
|
+
return DAEMON_AUDIT_ACTIONS.STARTED;
|
|
3348
|
+
case "online":
|
|
3349
|
+
return DAEMON_AUDIT_ACTIONS.ONLINE;
|
|
3350
|
+
case "draining":
|
|
3351
|
+
return DAEMON_AUDIT_ACTIONS.DRAINING;
|
|
3352
|
+
case "stopped":
|
|
3353
|
+
return DAEMON_AUDIT_ACTIONS.STOPPED;
|
|
3354
|
+
default:
|
|
3355
|
+
return null;
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
function readPid(pidFile) {
|
|
3359
|
+
try {
|
|
3360
|
+
const raw = fs6.readFileSync(pidFile, "utf8").trim();
|
|
3361
|
+
const parsed = Number.parseInt(raw, 10);
|
|
3362
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
3363
|
+
} catch {
|
|
3364
|
+
return null;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
function writePid(pidFile) {
|
|
3368
|
+
const dir = pidFile.replace(/\/[^/]+$/, "");
|
|
3369
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
3370
|
+
fs6.writeFileSync(pidFile, String(process.pid), { mode: 384 });
|
|
3371
|
+
}
|
|
3372
|
+
function removePid(pidFile) {
|
|
3373
|
+
try {
|
|
3374
|
+
fs6.unlinkSync(pidFile);
|
|
3375
|
+
} catch {
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
function isPidAlive(pidFile) {
|
|
3379
|
+
const pid = readPid(pidFile);
|
|
3380
|
+
if (pid == null) return false;
|
|
3381
|
+
return isProcessAlive(pid);
|
|
3382
|
+
}
|
|
3383
|
+
function isProcessAlive(pid) {
|
|
3384
|
+
try {
|
|
3385
|
+
process.kill(pid, 0);
|
|
3386
|
+
return true;
|
|
3387
|
+
} catch (err) {
|
|
3388
|
+
if (err.code === "EPERM") {
|
|
3389
|
+
return true;
|
|
3390
|
+
}
|
|
3391
|
+
return false;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
|
|
2849
3395
|
// _apps/@onklave/agent-cli/src/commands/index.ts
|
|
2850
3396
|
var COMMANDS = {
|
|
2851
3397
|
login: loginCommand,
|
|
@@ -2864,7 +3410,8 @@ var COMMANDS = {
|
|
|
2864
3410
|
init: (_args) => initCommand(),
|
|
2865
3411
|
config: configCommand,
|
|
2866
3412
|
register: registerCommand,
|
|
2867
|
-
logs: logsCommand
|
|
3413
|
+
logs: logsCommand,
|
|
3414
|
+
daemon: daemonCommand
|
|
2868
3415
|
};
|
|
2869
3416
|
|
|
2870
3417
|
// _apps/@onklave/agent-cli/src/main.ts
|