unitup 0.1.0 → 0.1.2
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/schedule.js +35 -39
- package/src/unit.js +4 -5
- package/src/utils.js +26 -1
package/package.json
CHANGED
package/src/schedule.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import {
|
|
4
5
|
sanitizeServiceName,
|
|
5
6
|
getUnitFilename,
|
|
6
7
|
getTimerFilename,
|
|
7
8
|
resolveAbsolutePath,
|
|
9
|
+
resolveWorkingDirectory,
|
|
8
10
|
validateDuration,
|
|
9
11
|
formatFutureTime,
|
|
10
12
|
formatRelativeTime,
|
|
@@ -106,42 +108,25 @@ export async function createSchedule(opts = {}) {
|
|
|
106
108
|
|
|
107
109
|
const safeName = sanitizeServiceName(rawName);
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
let execArgs = [];
|
|
111
|
-
let detectedRuntime = opts.runtime || 'node';
|
|
112
|
-
let scriptPath = '';
|
|
113
|
-
|
|
114
|
-
if (opts.command) {
|
|
115
|
-
commandExec = resolveAbsolutePath(opts.command);
|
|
116
|
-
execArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
117
|
-
if (opts.script) {
|
|
118
|
-
scriptPath = resolveAbsolutePath(opts.script);
|
|
119
|
-
}
|
|
120
|
-
} else if (opts.script) {
|
|
121
|
-
scriptPath = resolveAbsolutePath(opts.script);
|
|
122
|
-
const runtimeInfo = detectRuntime(scriptPath, opts.runtime);
|
|
123
|
-
detectedRuntime = runtimeInfo.runtime;
|
|
124
|
-
|
|
125
|
-
const resolved = await resolveRuntimeConfig({
|
|
126
|
-
runtime: detectedRuntime,
|
|
127
|
-
script: scriptPath,
|
|
128
|
-
command: opts.command,
|
|
129
|
-
args: opts.args || [],
|
|
130
|
-
runtimeArgs: opts.runtimeArgs || [],
|
|
131
|
-
customNodePath: opts.node
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
commandExec = resolved.command;
|
|
135
|
-
execArgs = resolved.args;
|
|
136
|
-
} else {
|
|
111
|
+
if (!opts.script && !opts.command) {
|
|
137
112
|
throw new Error('Either script or command must be provided to create schedule.');
|
|
138
113
|
}
|
|
139
114
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
:
|
|
143
|
-
|
|
144
|
-
:
|
|
115
|
+
const scriptPath = opts.script ? resolveAbsolutePath(opts.script) : '';
|
|
116
|
+
const resolved = await resolveRuntimeConfig({
|
|
117
|
+
runtime: opts.runtime,
|
|
118
|
+
script: scriptPath,
|
|
119
|
+
command: opts.command,
|
|
120
|
+
args: opts.args || [],
|
|
121
|
+
runtimeArgs: opts.runtimeArgs || [],
|
|
122
|
+
customNodePath: opts.node
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const commandExec = resolved.command;
|
|
126
|
+
const execArgs = resolved.args;
|
|
127
|
+
const detectedRuntime = resolved.runtime;
|
|
128
|
+
|
|
129
|
+
const cwd = resolveWorkingDirectory({ cwd: opts.cwd, script: scriptPath });
|
|
145
130
|
|
|
146
131
|
const envObj = {};
|
|
147
132
|
if (opts.env) {
|
|
@@ -185,6 +170,7 @@ export async function createSchedule(opts = {}) {
|
|
|
185
170
|
|
|
186
171
|
if (opts.start || opts.enable) {
|
|
187
172
|
await runCommand('systemctl', ['--user', 'daemon-reload']);
|
|
173
|
+
await runCommand('systemctl', ['--user', 'reset-failed', getUnitFilename(safeName), getTimerFilename(safeName)]);
|
|
188
174
|
await runCommand('systemctl', ['--user', 'enable', getTimerFilename(safeName)]);
|
|
189
175
|
await runCommand('systemctl', ['--user', 'start', getTimerFilename(safeName)]);
|
|
190
176
|
}
|
|
@@ -224,6 +210,7 @@ export async function enableSchedule(name) {
|
|
|
224
210
|
throw new Error(`Schedule timer unit up for "${name}" does not exist.`);
|
|
225
211
|
}
|
|
226
212
|
await runCommand('systemctl', ['--user', 'daemon-reload']);
|
|
213
|
+
await runCommand('systemctl', ['--user', 'reset-failed', getUnitFilename(safeName), getTimerFilename(safeName)]);
|
|
227
214
|
const res = await runCommand('systemctl', ['--user', 'enable', '--now', getTimerFilename(safeName)]);
|
|
228
215
|
return res.code === 0;
|
|
229
216
|
}
|
|
@@ -333,7 +320,7 @@ export async function getScheduleStatus(name) {
|
|
|
333
320
|
'--user',
|
|
334
321
|
'show',
|
|
335
322
|
timerFilename,
|
|
336
|
-
'--property=ActiveState,SubState,NextElapseUSecRealtime,NextElapseUSec,LastTriggerUSec,Unit,Triggers,UnitFileState'
|
|
323
|
+
'--property=ActiveState,SubState,NextElapseUSecRealtime,NextElapseUSecMonotonic,NextElapseUSec,LastTriggerUSec,Unit,Triggers,UnitFileState'
|
|
337
324
|
]);
|
|
338
325
|
|
|
339
326
|
const showServiceRes = await runCommand('systemctl', [
|
|
@@ -374,18 +361,27 @@ export async function getScheduleStatus(name) {
|
|
|
374
361
|
|
|
375
362
|
// Parse NextElapse timestamp
|
|
376
363
|
let rawNext = timerProps.NextElapseUSecRealtime || timerProps.NextElapseUSec;
|
|
377
|
-
if (!rawNext || rawNext === '0' || rawNext === '18446744073709551615') {
|
|
378
|
-
rawNext = 'n/a';
|
|
379
|
-
}
|
|
380
|
-
|
|
381
364
|
let nextRun = 'n/a';
|
|
382
|
-
|
|
365
|
+
|
|
366
|
+
if (rawNext && rawNext !== '0' && rawNext !== '18446744073709551615') {
|
|
383
367
|
const num = Number(rawNext);
|
|
384
368
|
if (!isNaN(num) && num > 0) {
|
|
385
369
|
nextRun = formatFutureTime(Math.floor(num / 1000));
|
|
386
370
|
} else {
|
|
387
371
|
nextRun = formatFutureTime(rawNext);
|
|
388
372
|
}
|
|
373
|
+
} else if (timerProps.NextElapseUSecMonotonic && timerProps.NextElapseUSecMonotonic !== '0' && timerProps.NextElapseUSecMonotonic !== '18446744073709551615') {
|
|
374
|
+
const monoNum = Number(timerProps.NextElapseUSecMonotonic);
|
|
375
|
+
if (!isNaN(monoNum) && monoNum > 0) {
|
|
376
|
+
const currentMonoUSec = os.uptime() * 1000000;
|
|
377
|
+
const diffUSec = monoNum - currentMonoUSec;
|
|
378
|
+
if (diffUSec > 0) {
|
|
379
|
+
const targetMs = Date.now() + Math.floor(diffUSec / 1000);
|
|
380
|
+
nextRun = formatFutureTime(targetMs);
|
|
381
|
+
} else {
|
|
382
|
+
nextRun = 'imminent';
|
|
383
|
+
}
|
|
384
|
+
}
|
|
389
385
|
}
|
|
390
386
|
|
|
391
387
|
// Parse LastTrigger timestamp
|
package/src/unit.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
formatSystemdEnv,
|
|
10
10
|
escapeExecArg,
|
|
11
11
|
resolveAbsolutePath,
|
|
12
|
+
resolveWorkingDirectory,
|
|
12
13
|
saveAppMetadata,
|
|
13
14
|
deleteAppMetadata,
|
|
14
15
|
saveScheduleMetadata,
|
|
@@ -83,8 +84,7 @@ export function generateUnitContent(opts) {
|
|
|
83
84
|
throw new Error('Either "command" or "script" must be provided to generate systemd unit file.');
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
const
|
|
87
|
-
const cwd = opts.cwd ? resolveAbsolutePath(opts.cwd) : path.dirname(scriptOrCmdPath || commandExec);
|
|
87
|
+
const cwd = resolveWorkingDirectory(opts);
|
|
88
88
|
const restartPolicy = opts.restart || 'on-failure';
|
|
89
89
|
|
|
90
90
|
const execStartTokens = [commandExec, ...execArgs];
|
|
@@ -306,8 +306,7 @@ export function generateScheduleServiceContent(opts) {
|
|
|
306
306
|
throw new Error('Either "command" or "script" must be provided to generate scheduled unit file.');
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
const
|
|
310
|
-
const cwd = opts.cwd ? resolveAbsolutePath(opts.cwd) : path.dirname(scriptOrCmdPath || commandExec);
|
|
309
|
+
const cwd = resolveWorkingDirectory(opts);
|
|
311
310
|
|
|
312
311
|
const execStartTokens = [commandExec, ...execArgs];
|
|
313
312
|
const execStartLine = execStartTokens.map(escapeExecArg).join(' ');
|
|
@@ -440,7 +439,7 @@ export function writeScheduleUnitFiles(opts) {
|
|
|
440
439
|
saveScheduleMetadata({
|
|
441
440
|
name: safeName,
|
|
442
441
|
group: opts.group || 'default',
|
|
443
|
-
runtime: opts.runtime || 'node',
|
|
442
|
+
runtime: opts.runtime || (opts.command ? 'custom' : 'node'),
|
|
444
443
|
command: opts.command,
|
|
445
444
|
args: opts.args,
|
|
446
445
|
cwd: opts.cwd,
|
package/src/utils.js
CHANGED
|
@@ -141,7 +141,7 @@ export function saveAppMetadata(meta) {
|
|
|
141
141
|
const rawArgs = Array.isArray(meta.args) ? meta.args : (meta.script ? [meta.script] : []);
|
|
142
142
|
const args = rawArgs.map(a => (a.startsWith('/') || a.startsWith('./') || a.startsWith('../') || a.startsWith('~/')) ? resolveAbsolutePath(a) : a);
|
|
143
143
|
const scriptPath = meta.script ? resolveAbsolutePath(meta.script) : (args[0] ? resolveAbsolutePath(args[0]) : command);
|
|
144
|
-
const cwd =
|
|
144
|
+
const cwd = resolveWorkingDirectory(meta);
|
|
145
145
|
|
|
146
146
|
// Extract memory resources if present
|
|
147
147
|
const resources = meta.resources || {};
|
|
@@ -302,6 +302,31 @@ export function resolveAbsolutePath(filepath, baseDir = process.cwd()) {
|
|
|
302
302
|
return path.resolve(baseDir, p);
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Safely resolves the working directory for a service or schedule unit.
|
|
307
|
+
*
|
|
308
|
+
* @param {Object} [opts]
|
|
309
|
+
* @returns {string}
|
|
310
|
+
*/
|
|
311
|
+
export function resolveWorkingDirectory(opts = {}) {
|
|
312
|
+
if (opts.cwd) {
|
|
313
|
+
return resolveAbsolutePath(opts.cwd);
|
|
314
|
+
}
|
|
315
|
+
if (opts.script) {
|
|
316
|
+
return path.dirname(resolveAbsolutePath(opts.script));
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const cwd = process.cwd();
|
|
320
|
+
if (fs.existsSync(cwd)) {
|
|
321
|
+
fs.accessSync(cwd, fs.constants.R_OK | fs.constants.X_OK);
|
|
322
|
+
return cwd;
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// fallback if process.cwd() is inaccessible
|
|
326
|
+
}
|
|
327
|
+
return os.homedir();
|
|
328
|
+
}
|
|
329
|
+
|
|
305
330
|
/**
|
|
306
331
|
* Escapes values for systemd Environment= line entries.
|
|
307
332
|
* E.g. KEY="val with spaces"
|