unitup 0.1.5 → 0.2.0
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/CHANGELOG.md +5 -3
- package/README.md +89 -8
- package/biome.json +0 -1
- package/docs/index.html +188 -234
- package/index.d.ts +336 -445
- package/package.json +9 -3
- package/src/cli.js +187 -101
- package/src/doctor.js +123 -56
- package/src/errors.js +131 -0
- package/src/index.js +132 -59
- package/src/logs.js +148 -0
- package/src/platform/base.js +211 -0
- package/src/platform/index.js +43 -0
- package/src/platform/linux.js +155 -0
- package/src/platform/macos.js +723 -0
- package/src/platform/windows-host.js +246 -0
- package/src/platform/windows.js +495 -0
- package/src/runtimes/bun.js +1 -1
- package/src/runtimes/common.js +24 -6
- package/src/runtimes/deno.js +1 -1
- package/src/runtimes/elixir.js +1 -1
- package/src/runtimes/go.js +1 -1
- package/src/runtimes/index.js +7 -7
- package/src/runtimes/native.js +2 -4
- package/src/runtimes/node.js +1 -1
- package/src/runtimes/php.js +1 -1
- package/src/runtimes/python.js +1 -1
- package/src/runtimes/ruby.js +1 -1
- package/src/runtimes/shell.js +1 -1
- package/src/schedule.js +31 -24
- package/src/service/config.js +55 -0
- package/src/service/manager.js +233 -0
- package/src/service/normalize.js +216 -0
- package/src/systemd.js +84 -38
- package/src/unit.js +12 -18
- package/src/utils.js +32 -18
- package/tsconfig.json +1 -1
- package/types-test/index.ts +158 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { getUnitupDir, readAppMetadata, saveAppMetadata } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Windows Service Host / Supervisor that manages a child process on Windows.
|
|
9
|
+
*/
|
|
10
|
+
export class WindowsServiceHost {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} config - Service configuration or metadata
|
|
13
|
+
*/
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.name = config.name;
|
|
17
|
+
this.child = null;
|
|
18
|
+
this.stopping = false;
|
|
19
|
+
this.restartCount = 0;
|
|
20
|
+
this.lastStartTime = null;
|
|
21
|
+
this.resetTimer = null;
|
|
22
|
+
this.logStreams = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initializes logging streams.
|
|
27
|
+
*/
|
|
28
|
+
initLogging() {
|
|
29
|
+
const stdoutPath = this.config.logs?.stdout || path.join(getUnitupDir(), 'logs', `${this.name}.log`);
|
|
30
|
+
const stderrPath = this.config.logs?.stderr || path.join(getUnitupDir(), 'logs', `${this.name}-error.log`);
|
|
31
|
+
|
|
32
|
+
const logDir = path.dirname(stdoutPath);
|
|
33
|
+
if (!fs.existsSync(logDir)) {
|
|
34
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.stdoutStream = fs.createWriteStream(stdoutPath, { flags: 'a' });
|
|
38
|
+
this.stderrStream = fs.createWriteStream(stderrPath, { flags: 'a' });
|
|
39
|
+
this.logStreams.push(this.stdoutStream, this.stderrStream);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Starts the supervised child process.
|
|
44
|
+
*/
|
|
45
|
+
start() {
|
|
46
|
+
if (this.stopping) return;
|
|
47
|
+
this.initLogging();
|
|
48
|
+
|
|
49
|
+
let command = this.config.command || process.execPath;
|
|
50
|
+
let args = Array.isArray(this.config.args) ? [...this.config.args] : [];
|
|
51
|
+
const cwd = this.config.cwd || process.cwd();
|
|
52
|
+
const env = { ...process.env, ...(this.config.env || {}) };
|
|
53
|
+
|
|
54
|
+
// On Windows, if executing a batch script, wrap with cmd.exe
|
|
55
|
+
if (process.platform === 'win32' && /\.(bat|cmd)$/i.test(command)) {
|
|
56
|
+
args = ['/d', '/s', '/c', `"${command}"`, ...args];
|
|
57
|
+
command = process.env.COMSPEC || 'cmd.exe';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.lastStartTime = new Date().toISOString();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
this.child = spawn(command, args, {
|
|
64
|
+
cwd,
|
|
65
|
+
env,
|
|
66
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
67
|
+
shell: false,
|
|
68
|
+
windowsHide: true
|
|
69
|
+
});
|
|
70
|
+
} catch (err) {
|
|
71
|
+
this.writeLog(`[unitup host] Failed to spawn process "${command}": ${err.message}\n`, true);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const pid = this.child.pid;
|
|
76
|
+
this.writeLog(`[unitup host] Service "${this.name}" started with PID ${pid} at ${this.lastStartTime}\n`);
|
|
77
|
+
|
|
78
|
+
// Reset restart count if process stays alive stably
|
|
79
|
+
const resetAfter = typeof this.config.restart?.resetAfter === 'number' ? this.config.restart.resetAfter : 60000;
|
|
80
|
+
if (this.resetTimer) clearTimeout(this.resetTimer);
|
|
81
|
+
this.resetTimer = setTimeout(() => {
|
|
82
|
+
this.restartCount = 0;
|
|
83
|
+
}, resetAfter);
|
|
84
|
+
|
|
85
|
+
// Update metadata with current PID
|
|
86
|
+
saveAppMetadata({
|
|
87
|
+
...this.config,
|
|
88
|
+
pid: String(pid),
|
|
89
|
+
started: this.lastStartTime,
|
|
90
|
+
restarts: String(this.restartCount)
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (this.child.stdout) {
|
|
94
|
+
this.child.stdout.pipe(this.stdoutStream, { end: false });
|
|
95
|
+
}
|
|
96
|
+
if (this.child.stderr) {
|
|
97
|
+
this.child.stderr.pipe(this.stderrStream, { end: false });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.child.on('error', (err) => {
|
|
101
|
+
this.writeLog(`[unitup host] Process error: ${err.message}\n`, true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.child.on('exit', (code, signal) => {
|
|
105
|
+
if (this.resetTimer) clearTimeout(this.resetTimer);
|
|
106
|
+
const exitMsg = `[unitup host] Process exited with code ${code}, signal ${signal}\n`;
|
|
107
|
+
this.writeLog(exitMsg);
|
|
108
|
+
|
|
109
|
+
this.child = null;
|
|
110
|
+
|
|
111
|
+
if (this.stopping) {
|
|
112
|
+
this.cleanup();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check restart policy
|
|
117
|
+
const restart = this.config.restart || {};
|
|
118
|
+
const shouldRestart = restart.enabled !== false && restart.policy !== 'no';
|
|
119
|
+
|
|
120
|
+
if (shouldRestart) {
|
|
121
|
+
const maxRetries = typeof restart.maxRetries === 'number' ? restart.maxRetries : Number.POSITIVE_INFINITY;
|
|
122
|
+
if (this.restartCount < maxRetries) {
|
|
123
|
+
this.restartCount++;
|
|
124
|
+
const delay = typeof restart.delay === 'number' ? restart.delay : 3000;
|
|
125
|
+
this.writeLog(`[unitup host] Restarting service in ${delay}ms (attempt ${this.restartCount})...\n`);
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
if (!this.stopping) {
|
|
128
|
+
this.start();
|
|
129
|
+
}
|
|
130
|
+
}, delay);
|
|
131
|
+
} else {
|
|
132
|
+
this.writeLog(`[unitup host] Maximum restart attempts (${maxRetries}) reached.\n`, true);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Writes message to stdout or stderr log.
|
|
140
|
+
*/
|
|
141
|
+
writeLog(msg, isError = false) {
|
|
142
|
+
const stream = isError ? this.stderrStream : this.stdoutStream;
|
|
143
|
+
if (stream && !stream.destroyed) {
|
|
144
|
+
stream.write(msg);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Stops the supervised child process gracefully.
|
|
150
|
+
*
|
|
151
|
+
* @param {number} [timeout]
|
|
152
|
+
* @returns {Promise<void>}
|
|
153
|
+
*/
|
|
154
|
+
async stop(timeout) {
|
|
155
|
+
this.stopping = true;
|
|
156
|
+
if (this.resetTimer) clearTimeout(this.resetTimer);
|
|
157
|
+
const shutdownTimeout = timeout || this.config.shutdownTimeout || 10000;
|
|
158
|
+
|
|
159
|
+
if (!this.child || !this.child.pid) {
|
|
160
|
+
this.cleanup();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const pid = this.child.pid;
|
|
165
|
+
this.writeLog(`[unitup host] Stopping service "${this.name}" (PID ${pid})...\n`);
|
|
166
|
+
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
let forceKillTimer = null;
|
|
169
|
+
|
|
170
|
+
const onExit = () => {
|
|
171
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
172
|
+
this.cleanup();
|
|
173
|
+
resolve();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (this.child) {
|
|
177
|
+
this.child.once('exit', onExit);
|
|
178
|
+
try {
|
|
179
|
+
this.child.kill('SIGTERM');
|
|
180
|
+
} catch {
|
|
181
|
+
// ignore
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
forceKillTimer = setTimeout(() => {
|
|
185
|
+
this.writeLog('[unitup host] Graceful shutdown timed out. Force killing process...\n', true);
|
|
186
|
+
if (process.platform === 'win32') {
|
|
187
|
+
try {
|
|
188
|
+
execFile('taskkill', ['/pid', String(pid), '/T', '/F'], () => {});
|
|
189
|
+
} catch {
|
|
190
|
+
// ignore
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (this.child) {
|
|
194
|
+
try {
|
|
195
|
+
this.child.kill('SIGKILL');
|
|
196
|
+
} catch {
|
|
197
|
+
// ignore
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
this.cleanup();
|
|
201
|
+
resolve();
|
|
202
|
+
}, shutdownTimeout);
|
|
203
|
+
} else {
|
|
204
|
+
resolve();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Closes open log streams and resources.
|
|
211
|
+
*/
|
|
212
|
+
cleanup() {
|
|
213
|
+
for (const s of this.logStreams) {
|
|
214
|
+
try {
|
|
215
|
+
if (s && !s.destroyed) s.end();
|
|
216
|
+
} catch {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Standalone runner execution entry point when run directly:
|
|
224
|
+
// node windows-host.js <serviceName>
|
|
225
|
+
if (process.argv[1]?.endsWith('windows-host.js') && process.argv[2]) {
|
|
226
|
+
const serviceName = process.argv[2];
|
|
227
|
+
const meta = readAppMetadata(serviceName);
|
|
228
|
+
if (!meta) {
|
|
229
|
+
process.stderr.write(`[unitup host] Could not read metadata for service "${serviceName}"\n`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const host = new WindowsServiceHost(meta);
|
|
234
|
+
|
|
235
|
+
process.on('SIGINT', async () => {
|
|
236
|
+
await host.stop();
|
|
237
|
+
process.exit(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
process.on('SIGTERM', async () => {
|
|
241
|
+
await host.stop();
|
|
242
|
+
process.exit(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
host.start();
|
|
246
|
+
}
|