@coworker-jp/aidr 0.1.3 → 0.1.4
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/package.json +1 -1
- package/src/cli.mjs +1 -1
- package/src/scheduled.mjs +27 -84
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
package/src/scheduled.mjs
CHANGED
|
@@ -22,24 +22,19 @@ export const SYSTEM_USR_LOCAL_BIN = ROOT_PREFIX + "/usr/local/bin/ai-scanner";
|
|
|
22
22
|
const SYSTEM_ENV_DIR = ROOT_PREFIX + "/etc/aidr";
|
|
23
23
|
export const SYSTEM_ENV_FILE = path.join(SYSTEM_ENV_DIR, "aidr.env");
|
|
24
24
|
|
|
25
|
-
// Linux systemd
|
|
26
|
-
const SYSTEMD_DIR = ROOT_PREFIX + "/etc/systemd/system";
|
|
27
|
-
export const SYSTEMD_SERVICE = path.join(SYSTEMD_DIR, "aidr-scheduled.service");
|
|
28
|
-
export const SYSTEMD_TIMER = path.join(SYSTEMD_DIR, "aidr-scheduled.timer");
|
|
29
|
-
|
|
30
25
|
// macOS launchd (LaunchDaemons run as root, available pre-login)
|
|
31
26
|
export const LAUNCHD_PLIST = ROOT_PREFIX + "/Library/LaunchDaemons/jp.coworker.aidr.scheduled.plist";
|
|
32
27
|
export const LAUNCHD_WRAPPER = path.join(SYSTEM_BIN_DIR, "aidr-scheduled-wrapper.sh");
|
|
33
28
|
|
|
34
|
-
// Linux cron
|
|
29
|
+
// Linux: cron.d entry, runs as root every N hours.
|
|
35
30
|
export const CRON_FILE = ROOT_PREFIX + "/etc/cron.d/aidr-scheduled";
|
|
36
31
|
|
|
37
32
|
// Shared world-readable system log directory. Both aidr-scheduled and
|
|
38
33
|
// coworker-sentinel daemons write here so non-root users can `tail` errors
|
|
39
34
|
// without sudo (memory: feedback_server_failures_never_burden_client.md).
|
|
40
|
-
// Mode 0755 root:root, files mode 0644.
|
|
41
|
-
//
|
|
42
|
-
//
|
|
35
|
+
// Mode 0755 root:root, files mode 0644. launchd's `StandardOutPath` (macOS)
|
|
36
|
+
// redirects daemon stderr/stdout here; for Linux cron, the entry uses a
|
|
37
|
+
// literal `>> ... 2>&1` redirect.
|
|
43
38
|
export const SYSTEM_LOG_DIR = ROOT_PREFIX + "/var/log/coworker";
|
|
44
39
|
export const SCHEDULED_LOG_FILE = path.join(SYSTEM_LOG_DIR, "scheduled.log");
|
|
45
40
|
export const SENTINEL_LOG_FILE = path.join(SYSTEM_LOG_DIR, "sentinel.log");
|
|
@@ -134,11 +129,7 @@ export async function removeSystemLog(logFile, { dryRun = false } = {}) {
|
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
export async function isScheduledInstalled() {
|
|
137
|
-
return (
|
|
138
|
-
await exists(SYSTEMD_TIMER) ||
|
|
139
|
-
await exists(LAUNCHD_PLIST) ||
|
|
140
|
-
await exists(CRON_FILE)
|
|
141
|
-
);
|
|
132
|
+
return (await exists(LAUNCHD_PLIST)) || (await exists(CRON_FILE));
|
|
142
133
|
}
|
|
143
134
|
|
|
144
135
|
export async function installScheduled(opts) {
|
|
@@ -162,15 +153,12 @@ export async function installScheduled(opts) {
|
|
|
162
153
|
await writeEnvFile(accessKey, dryRun);
|
|
163
154
|
|
|
164
155
|
// Create the world-readable log dir + touch scheduled.log + drop the
|
|
165
|
-
// shared logrotate config. Both run before the
|
|
166
|
-
//
|
|
156
|
+
// shared logrotate config. Both run before the launchd / cron entry so
|
|
157
|
+
// the daemon's first invocation has a target to append to.
|
|
167
158
|
await ensureSystemLogDir(SCHEDULED_LOG_FILE, { dryRun });
|
|
168
159
|
await writeLogrotateConfig({ dryRun });
|
|
169
160
|
|
|
170
|
-
|
|
171
|
-
if (platform === "linux") {
|
|
172
|
-
await installSystemd({ dryRun, noActivate, interval, binaryPath });
|
|
173
|
-
} else if (platform === "darwin") {
|
|
161
|
+
if (process.platform === "darwin") {
|
|
174
162
|
await installLaunchd({ dryRun, noActivate, interval, binaryPath });
|
|
175
163
|
} else {
|
|
176
164
|
await installCronDir({ binaryPath, dryRun, interval });
|
|
@@ -197,57 +185,6 @@ async function writeEnvFile(accessKey, dryRun) {
|
|
|
197
185
|
await fs.writeFile(SYSTEM_ENV_FILE, envContent, { mode: 0o600 });
|
|
198
186
|
}
|
|
199
187
|
|
|
200
|
-
async function installSystemd({ binaryPath, dryRun, noActivate, interval = 4 }) {
|
|
201
|
-
// StandardOutput=append: requires systemd 240+ (released 2018). Modern
|
|
202
|
-
// distros (RHEL 9 / Ubuntu 20.04+ / Debian 11+) all support it. Older
|
|
203
|
-
// RHEL 8 (systemd 239) operators must redirect manually or upgrade.
|
|
204
|
-
const serviceContent = `[Unit]
|
|
205
|
-
Description=AIDR scheduled endpoint-info scan
|
|
206
|
-
After=network-online.target
|
|
207
|
-
Wants=network-online.target
|
|
208
|
-
|
|
209
|
-
[Service]
|
|
210
|
-
Type=oneshot
|
|
211
|
-
User=root
|
|
212
|
-
EnvironmentFile=${SYSTEM_ENV_FILE}
|
|
213
|
-
ExecStart=${binaryPath} scan endpoint-info
|
|
214
|
-
StandardOutput=append:${SCHEDULED_LOG_FILE}
|
|
215
|
-
StandardError=append:${SCHEDULED_LOG_FILE}
|
|
216
|
-
`;
|
|
217
|
-
|
|
218
|
-
const timerContent = `[Unit]
|
|
219
|
-
Description=AIDR scheduled endpoint-info scan timer
|
|
220
|
-
|
|
221
|
-
[Timer]
|
|
222
|
-
OnBootSec=5min
|
|
223
|
-
OnUnitActiveSec=${interval}h
|
|
224
|
-
Persistent=true
|
|
225
|
-
|
|
226
|
-
[Install]
|
|
227
|
-
WantedBy=timers.target
|
|
228
|
-
`;
|
|
229
|
-
|
|
230
|
-
if (dryRun) {
|
|
231
|
-
console.log("[dry-run] Would create:", SYSTEMD_SERVICE, SYSTEMD_TIMER);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
await fs.mkdir(path.dirname(SYSTEMD_SERVICE), { recursive: true });
|
|
236
|
-
await fs.writeFile(SYSTEMD_SERVICE, serviceContent, { mode: 0o644 });
|
|
237
|
-
await fs.writeFile(SYSTEMD_TIMER, timerContent, { mode: 0o644 });
|
|
238
|
-
|
|
239
|
-
if (!noActivate) {
|
|
240
|
-
try {
|
|
241
|
-
await execFileP("systemctl", ["daemon-reload"]);
|
|
242
|
-
await execFileP("systemctl", ["enable", "--now", "aidr-scheduled.timer"]);
|
|
243
|
-
console.log(`Scheduled scan enabled via system systemd timer (every ${interval}h)`);
|
|
244
|
-
} catch (e) {
|
|
245
|
-
console.warn("Warning: systemctl failed:", e.message);
|
|
246
|
-
console.warn("Timer files were written. Enable manually: sudo systemctl enable --now aidr-scheduled.timer");
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
188
|
export async function installLaunchd({ binaryPath, dryRun, noActivate, interval = 4 }) {
|
|
252
189
|
// launchd does not support EnvironmentFile, so we use a tiny wrapper that
|
|
253
190
|
// sources /etc/aidr/aidr.env and execs the binary. Plist itself stays 0644
|
|
@@ -318,7 +255,7 @@ async function installCronDir({ binaryPath, dryRun, interval = 4 }) {
|
|
|
318
255
|
// /etc/cron.d entries run as the user named in the line. We embed the env
|
|
319
256
|
// file source via a small inline script. stdout + stderr are appended to
|
|
320
257
|
// the world-readable log so non-root users can read recent runs without
|
|
321
|
-
// sudo
|
|
258
|
+
// sudo.
|
|
322
259
|
const entry = `0 */${interval} * * * root . ${SYSTEM_ENV_FILE} && ${binaryPath} scan endpoint-info >> ${SCHEDULED_LOG_FILE} 2>&1 # aidr-scheduled
|
|
323
260
|
`;
|
|
324
261
|
if (dryRun) {
|
|
@@ -328,6 +265,22 @@ async function installCronDir({ binaryPath, dryRun, interval = 4 }) {
|
|
|
328
265
|
await fs.mkdir(path.dirname(CRON_FILE), { recursive: true });
|
|
329
266
|
await fs.writeFile(CRON_FILE, entry, { mode: 0o644 });
|
|
330
267
|
console.log(`Scheduled scan added to ${CRON_FILE} (every ${interval}h)`);
|
|
268
|
+
|
|
269
|
+
// Warn (don't fail) when no cron daemon is detected. The file is valid;
|
|
270
|
+
// the user just needs to install one (`apt install cron`, `dnf install
|
|
271
|
+
// cronie`, etc.) before the next scheduled hour for it to fire.
|
|
272
|
+
if (!await hasCronDaemon()) {
|
|
273
|
+
console.warn(
|
|
274
|
+
"WARNING: cron daemon not detected. Install one (`apt install cron` / `dnf install cronie` / `apk add dcron`) so /etc/cron.d/aidr-scheduled actually fires."
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function hasCronDaemon() {
|
|
280
|
+
for (const candidate of ["/usr/sbin/cron", "/usr/sbin/crond", "/sbin/cron", "/sbin/crond"]) {
|
|
281
|
+
if (await exists(candidate)) return true;
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
331
284
|
}
|
|
332
285
|
|
|
333
286
|
export async function uninstallScheduled({ dryRun = false, noActivate = false } = {}) {
|
|
@@ -337,16 +290,6 @@ export async function uninstallScheduled({ dryRun = false, noActivate = false }
|
|
|
337
290
|
throw new Error("uninstallScheduled requires root privileges");
|
|
338
291
|
}
|
|
339
292
|
|
|
340
|
-
if (await exists(SYSTEMD_TIMER)) {
|
|
341
|
-
if (!dryRun && !noActivate) {
|
|
342
|
-
try { await execFileP("systemctl", ["disable", "--now", "aidr-scheduled.timer"]); } catch { /* ignore */ }
|
|
343
|
-
}
|
|
344
|
-
for (const f of [SYSTEMD_SERVICE, SYSTEMD_TIMER]) {
|
|
345
|
-
if (!dryRun) { try { await fs.unlink(f); } catch { /* ignore */ } }
|
|
346
|
-
else { console.log("[dry-run] Would remove:", f); }
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
293
|
if (await exists(LAUNCHD_PLIST)) {
|
|
351
294
|
if (!dryRun && !noActivate) {
|
|
352
295
|
try { await execFileP("launchctl", ["bootout", "system", LAUNCHD_PLIST]); } catch { /* ignore */ }
|
|
@@ -364,8 +307,8 @@ export async function uninstallScheduled({ dryRun = false, noActivate = false }
|
|
|
364
307
|
else { console.log("[dry-run] Would remove:", CRON_FILE); }
|
|
365
308
|
}
|
|
366
309
|
|
|
367
|
-
// Remove the env file last so an aborted uninstall
|
|
368
|
-
//
|
|
310
|
+
// Remove the env file last so an aborted uninstall doesn't strand a
|
|
311
|
+
// schedule entry that still references a missing env file.
|
|
369
312
|
if (await exists(SYSTEM_ENV_FILE)) {
|
|
370
313
|
if (!dryRun) { try { await fs.unlink(SYSTEM_ENV_FILE); } catch { /* ignore */ } }
|
|
371
314
|
else { console.log("[dry-run] Would remove:", SYSTEM_ENV_FILE); }
|