unitup 0.1.5 → 0.3.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/.agents/AGENTS.md +35 -0
- package/.agents/skills/multi-runtime-management/SKILL.md +80 -0
- package/.agents/skills/platform-service-adapters/SKILL.md +108 -0
- package/.agents/skills/unitup-development-and-qa/SKILL.md +88 -0
- package/.agents/skills/zero-downtime-deployment/SKILL.md +135 -0
- 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 +467 -440
- package/package.json +9 -3
- package/src/cli.js +561 -103
- package/src/core/drain-manager.js +70 -0
- package/src/core/generation-manager.js +414 -0
- package/src/core/index.js +8 -0
- package/src/core/ipc.js +236 -0
- package/src/core/lock.js +133 -0
- package/src/core/process-manager.js +313 -0
- package/src/core/readiness.js +243 -0
- package/src/core/router.js +390 -0
- package/src/core/supervisor.js +222 -0
- package/src/deploy/deployment-manager.js +407 -0
- package/src/deploy/index.js +9 -0
- package/src/deploy/rollback-manager.js +186 -0
- package/src/doctor.js +123 -56
- package/src/errors.js +131 -0
- package/src/index.js +186 -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 +726 -0
- package/src/platform/windows-host.js +246 -0
- package/src/platform/windows.js +498 -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 +63 -0
- package/src/service/manager.js +233 -0
- package/src/service/normalize.js +245 -0
- package/src/systemd.js +84 -38
- package/src/unit.js +17 -19
- package/src/utils.js +112 -18
- package/tsconfig.json +1 -1
- package/types-test/index.ts +158 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { DrainManager } from '../core/drain-manager.js';
|
|
2
|
+
import { GenerationManager } from '../core/generation-manager.js';
|
|
3
|
+
import { IPCClient } from '../core/ipc.js';
|
|
4
|
+
import { DeploymentLock } from '../core/lock.js';
|
|
5
|
+
import { ProcessManager } from '../core/process-manager.js';
|
|
6
|
+
import { ReadinessChecker } from '../core/readiness.js';
|
|
7
|
+
import { Router } from '../core/router.js';
|
|
8
|
+
import { readAppMetadata } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* RollbackManager provides orchestration for reverting traffic to a previous generation.
|
|
12
|
+
* It strictly composes the Unitup core primitives.
|
|
13
|
+
*/
|
|
14
|
+
export class RollbackManager {
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} [options]
|
|
17
|
+
* @param {import('../core/generation-manager.js').GenerationManager} [options.generationManager]
|
|
18
|
+
* @param {import('../core/router.js').Router} [options.router]
|
|
19
|
+
* @param {import('../core/readiness.js').ReadinessChecker} [options.readiness]
|
|
20
|
+
* @param {import('../core/drain-manager.js').DrainManager} [options.drainManager]
|
|
21
|
+
* @param {import('../core/process-manager.js').ProcessManager} [options.processManager]
|
|
22
|
+
* @param {import('../core/lock.js').DeploymentLock} [options.lock]
|
|
23
|
+
*/
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.generationManager = options.generationManager;
|
|
26
|
+
this.router = options.router;
|
|
27
|
+
this.readiness = options.readiness;
|
|
28
|
+
this.drainManager = options.drainManager;
|
|
29
|
+
this.processManager = options.processManager;
|
|
30
|
+
this.lock = options.lock;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_ensurePrimitives(serviceName, options = {}) {
|
|
34
|
+
if (!this.processManager) {
|
|
35
|
+
this.processManager = new ProcessManager();
|
|
36
|
+
}
|
|
37
|
+
if (!this.generationManager) {
|
|
38
|
+
this.generationManager = new GenerationManager({
|
|
39
|
+
processManager: this.processManager,
|
|
40
|
+
stateDir: options.stateDir
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (!this.router) {
|
|
44
|
+
this.router = new Router();
|
|
45
|
+
}
|
|
46
|
+
if (!this.readiness) {
|
|
47
|
+
this.readiness = new ReadinessChecker({ processManager: this.processManager });
|
|
48
|
+
}
|
|
49
|
+
if (!this.drainManager) {
|
|
50
|
+
this.drainManager = new DrainManager({
|
|
51
|
+
router: this.router,
|
|
52
|
+
processManager: this.processManager
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (!this.lock) {
|
|
56
|
+
this.lock = new DeploymentLock({
|
|
57
|
+
lockDir: options.lockDir,
|
|
58
|
+
processManager: this.processManager
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Rolls back a service to its previous generation.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} serviceName
|
|
67
|
+
* @param {object} [serviceConfig]
|
|
68
|
+
* @param {object} [options]
|
|
69
|
+
* @param {string} [options.readyPath]
|
|
70
|
+
* @param {number} [options.drainTimeout]
|
|
71
|
+
* @param {function(object): void} [options.onProgress]
|
|
72
|
+
* @returns {Promise<object>} Rollback summary
|
|
73
|
+
*/
|
|
74
|
+
async rollback(serviceName, serviceConfig = {}, options = {}) {
|
|
75
|
+
if (!serviceName) {
|
|
76
|
+
throw new Error('Service name is required for rollback.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check if a supervisor daemon is running for this service via IPC
|
|
80
|
+
const ipcClient = new IPCClient(serviceName, options.ipcDir);
|
|
81
|
+
const isRemote = await ipcClient.isAlive();
|
|
82
|
+
if (isRemote) {
|
|
83
|
+
return ipcClient.send(
|
|
84
|
+
{
|
|
85
|
+
action: 'rollback',
|
|
86
|
+
options: { ...options, ...serviceConfig }
|
|
87
|
+
},
|
|
88
|
+
options.onProgress
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this._ensurePrimitives(serviceName, options);
|
|
93
|
+
|
|
94
|
+
const config =
|
|
95
|
+
serviceConfig && (serviceConfig.command || serviceConfig.script)
|
|
96
|
+
? serviceConfig
|
|
97
|
+
: readAppMetadata(serviceName) || serviceConfig || {};
|
|
98
|
+
|
|
99
|
+
await this.lock.acquire(serviceName);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Check if a canary generation is running: if so, abort canary and preserve active
|
|
103
|
+
const canary = this.generationManager.getCanary(serviceName);
|
|
104
|
+
if (canary) {
|
|
105
|
+
if (options.onProgress) {
|
|
106
|
+
options.onProgress({ state: 'ABORTING_CANARY', canaryGeneration: canary.id });
|
|
107
|
+
}
|
|
108
|
+
this.router.clearCanary(serviceName);
|
|
109
|
+
const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 5000;
|
|
110
|
+
await this.drainManager.drain(canary, { timeout: drainTimeout, stopProcess: true });
|
|
111
|
+
this.generationManager.remove(canary, false);
|
|
112
|
+
|
|
113
|
+
const active = this.generationManager.getActive(serviceName);
|
|
114
|
+
return {
|
|
115
|
+
service: serviceName,
|
|
116
|
+
abortedCanary: canary.id,
|
|
117
|
+
activeGeneration: active ? active.id : null,
|
|
118
|
+
status: 'canary_aborted'
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const current = this.generationManager.getActive(serviceName);
|
|
123
|
+
const previous = this.generationManager.getPrevious(serviceName);
|
|
124
|
+
|
|
125
|
+
if (!previous) {
|
|
126
|
+
throw new Error(`No previous generation available to roll back to for service "${serviceName}".`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (options.onProgress) {
|
|
130
|
+
options.onProgress({ state: 'CHECKING_PREVIOUS', previous: previous.id });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if previous is alive
|
|
134
|
+
if (!this.processManager.isAlive(previous.pid)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Previous generation #${previous.id} (PID ${previous.pid}) is no longer running and cannot be rolled back to.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Ensure previous generation is still ready
|
|
141
|
+
const isReady = await this.readiness.ensure(previous, {
|
|
142
|
+
path: options.readyPath || config.deploy?.ready
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!isReady) {
|
|
146
|
+
throw new Error(`Previous generation #${previous.id} failed readiness verification.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (options.onProgress) {
|
|
150
|
+
options.onProgress({ state: 'SWITCHING', targetGeneration: previous.id });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Switch router to previous generation
|
|
154
|
+
this.router.switchBackend(serviceName, previous);
|
|
155
|
+
this.generationManager.markActive(previous);
|
|
156
|
+
|
|
157
|
+
// Drain and stop the rolled-back current generation
|
|
158
|
+
if (current && current.id !== previous.id) {
|
|
159
|
+
if (options.onProgress) {
|
|
160
|
+
options.onProgress({ state: 'DRAINING_CURRENT', generation: current.id });
|
|
161
|
+
}
|
|
162
|
+
this.generationManager.markDraining(current);
|
|
163
|
+
const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 10000;
|
|
164
|
+
await this.drainManager.drain(current, { timeout: drainTimeout, stopProcess: true });
|
|
165
|
+
this.generationManager.remove(current, false);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const result = {
|
|
169
|
+
service: serviceName,
|
|
170
|
+
rolledBackFrom: current?.id ?? null,
|
|
171
|
+
activeGeneration: previous.id,
|
|
172
|
+
status: 'rolled_back'
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (options.onProgress) {
|
|
176
|
+
options.onProgress({ state: 'COMPLETE', ...result });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
} finally {
|
|
181
|
+
await this.lock.release(serviceName);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const defaultRollbackManager = new RollbackManager();
|
package/src/doctor.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import os from 'node:os';
|
|
3
|
+
import { findRuntimeExecutable } from './runtimes/common.js';
|
|
2
4
|
import {
|
|
5
|
+
checkNodeDiagnostics,
|
|
6
|
+
checkUserLinger,
|
|
3
7
|
isLinux,
|
|
4
8
|
isSystemctlAvailable,
|
|
5
9
|
isSystemdPID1,
|
|
6
10
|
isUserSystemdAvailable,
|
|
7
11
|
isUserUnitDirWritable,
|
|
8
|
-
|
|
9
|
-
checkNodeDiagnostics
|
|
12
|
+
runCommand
|
|
10
13
|
} from './systemd.js';
|
|
11
14
|
import { getUserUnitDir } from './unit.js';
|
|
12
|
-
import {
|
|
15
|
+
import { getUnitupDir } from './utils.js';
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Runs system diagnostic checks and returns structured data.
|
|
@@ -17,6 +20,7 @@ import { findRuntimeExecutable } from './runtimes/common.js';
|
|
|
17
20
|
* @returns {Promise<Object>}
|
|
18
21
|
*/
|
|
19
22
|
export async function getDoctorInfo() {
|
|
23
|
+
const platform = process.platform;
|
|
20
24
|
const linux = isLinux();
|
|
21
25
|
const systemctl = await isSystemctlAvailable();
|
|
22
26
|
const pid1 = await isSystemdPID1();
|
|
@@ -25,6 +29,32 @@ export async function getDoctorInfo() {
|
|
|
25
29
|
const lingering = await checkUserLinger();
|
|
26
30
|
const nodeDiag = await checkNodeDiagnostics();
|
|
27
31
|
|
|
32
|
+
// macOS specific checks
|
|
33
|
+
let launchctlAvailable = false;
|
|
34
|
+
if (platform === 'darwin') {
|
|
35
|
+
try {
|
|
36
|
+
const res = await runCommand('launchctl', ['version']);
|
|
37
|
+
launchctlAvailable = res.code === 0 || (res.stdout && res.stdout.length > 0) || !res.stderr.includes('not found');
|
|
38
|
+
} catch {
|
|
39
|
+
launchctlAvailable = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Windows specific checks
|
|
44
|
+
let windowsScmAvailable = false;
|
|
45
|
+
let isAdmin = false;
|
|
46
|
+
if (platform === 'win32') {
|
|
47
|
+
try {
|
|
48
|
+
const res = await runCommand('sc.exe', ['query']);
|
|
49
|
+
windowsScmAvailable = res.code === 0 || !res.stderr.includes('not found');
|
|
50
|
+
// Simple admin check: net session or whoami /priv
|
|
51
|
+
const adminRes = await runCommand('net', ['session']);
|
|
52
|
+
isAdmin = adminRes.code === 0;
|
|
53
|
+
} catch {
|
|
54
|
+
windowsScmAvailable = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
28
58
|
const nodePath = nodeDiag.execPath || (await findRuntimeExecutable(['node']));
|
|
29
59
|
const pythonPath = await findRuntimeExecutable(['python3', 'python']);
|
|
30
60
|
const rubyPath = await findRuntimeExecutable(['ruby']);
|
|
@@ -36,13 +66,13 @@ export async function getDoctorInfo() {
|
|
|
36
66
|
|
|
37
67
|
const runtimes = {
|
|
38
68
|
'Node.js': nodePath,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
Python: pythonPath,
|
|
70
|
+
Ruby: rubyPath,
|
|
71
|
+
PHP: phpPath,
|
|
72
|
+
Bun: bunPath,
|
|
73
|
+
Deno: denoPath,
|
|
74
|
+
Go: goPath,
|
|
75
|
+
Elixir: elixirPath
|
|
46
76
|
};
|
|
47
77
|
|
|
48
78
|
let cgroupV2 = false;
|
|
@@ -60,11 +90,20 @@ export async function getDoctorInfo() {
|
|
|
60
90
|
const memoryMaxSupported = linux && (cgroupV2 || userSystemd);
|
|
61
91
|
const memorySwapMaxSupported = linux && cgroupV2 && memoryController;
|
|
62
92
|
|
|
93
|
+
let serviceManager = 'systemd';
|
|
94
|
+
if (platform === 'darwin') serviceManager = 'launchd';
|
|
95
|
+
else if (platform === 'win32') serviceManager = 'Windows Service Manager';
|
|
96
|
+
|
|
63
97
|
return {
|
|
98
|
+
platform,
|
|
99
|
+
serviceManager,
|
|
64
100
|
linux,
|
|
65
101
|
systemctl,
|
|
66
102
|
systemdRunning: pid1,
|
|
67
103
|
userSystemdAvailable: userSystemd,
|
|
104
|
+
launchctlAvailable,
|
|
105
|
+
windowsScmAvailable,
|
|
106
|
+
isAdmin,
|
|
68
107
|
cgroupV2,
|
|
69
108
|
memoryController,
|
|
70
109
|
memoryMaxSupported,
|
|
@@ -74,6 +113,7 @@ export async function getDoctorInfo() {
|
|
|
74
113
|
nodeVersion: nodeDiag.version,
|
|
75
114
|
runtimes,
|
|
76
115
|
unitDir: getUserUnitDir(),
|
|
116
|
+
unitupDataDir: getUnitupDir(),
|
|
77
117
|
unitDirWritable,
|
|
78
118
|
lingering,
|
|
79
119
|
username: os.userInfo().username
|
|
@@ -89,27 +129,49 @@ export async function runDoctor() {
|
|
|
89
129
|
const info = await getDoctorInfo();
|
|
90
130
|
|
|
91
131
|
if (info.linux) {
|
|
132
|
+
console.log('Platform: Linux');
|
|
133
|
+
console.log('Service manager: systemd');
|
|
134
|
+
console.log(`Node: ${info.nodePath} ${info.nodeVersion ? `(${info.nodeVersion})` : ''}`);
|
|
135
|
+
console.log(`Unitup data: ${info.unitupDataDir}`);
|
|
92
136
|
console.log('✓ Linux detected');
|
|
93
|
-
} else {
|
|
94
|
-
console.log('✗ Non-Linux OS detected (systemd requires Linux)');
|
|
95
|
-
}
|
|
96
137
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
138
|
+
if (info.systemctl) {
|
|
139
|
+
console.log('✓ systemctl available');
|
|
140
|
+
} else {
|
|
141
|
+
console.log('✗ systemctl command not found');
|
|
142
|
+
}
|
|
102
143
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
144
|
+
if (info.systemdRunning) {
|
|
145
|
+
console.log('✓ systemd is running');
|
|
146
|
+
} else {
|
|
147
|
+
console.log('! systemd is not running as PID 1');
|
|
148
|
+
}
|
|
108
149
|
|
|
109
|
-
|
|
110
|
-
|
|
150
|
+
if (info.userSystemdAvailable) {
|
|
151
|
+
console.log('✓ systemd user services available');
|
|
152
|
+
} else {
|
|
153
|
+
console.log('! systemd user services unavailable');
|
|
154
|
+
}
|
|
155
|
+
} else if (info.platform === 'darwin') {
|
|
156
|
+
console.log('Platform: macOS');
|
|
157
|
+
console.log('Service manager: launchd');
|
|
158
|
+
console.log(`Node: ${info.nodePath} ${info.nodeVersion ? `(${info.nodeVersion})` : ''}`);
|
|
159
|
+
console.log(`Unitup data: ${info.unitupDataDir}`);
|
|
160
|
+
console.log('! Non-Linux OS detected (macOS uses launchd)');
|
|
161
|
+
console.log('✓ launchctl: available');
|
|
162
|
+
console.log('✓ Permissions: user services supported');
|
|
163
|
+
} else if (info.platform === 'win32') {
|
|
164
|
+
console.log('Platform: Windows');
|
|
165
|
+
console.log('Service manager: Windows Service Manager');
|
|
166
|
+
console.log(`Node: ${info.nodePath} ${info.nodeVersion ? `(${info.nodeVersion})` : ''}`);
|
|
167
|
+
console.log(`Unitup data: ${info.unitupDataDir}`);
|
|
168
|
+
console.log('! Non-Linux OS detected (Windows uses Windows Services)');
|
|
169
|
+
console.log(`Administrator: ${info.isAdmin ? 'yes' : 'no'}`);
|
|
170
|
+
if (!info.isAdmin) {
|
|
171
|
+
console.log('! Service installation requires Administrator privileges');
|
|
172
|
+
}
|
|
111
173
|
} else {
|
|
112
|
-
console.log(
|
|
174
|
+
console.log(`✗ Non-Linux OS detected (${info.platform})`);
|
|
113
175
|
}
|
|
114
176
|
|
|
115
177
|
console.log('\nDetected runtimes:');
|
|
@@ -121,48 +183,53 @@ export async function runDoctor() {
|
|
|
121
183
|
}
|
|
122
184
|
}
|
|
123
185
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
186
|
+
if (info.linux) {
|
|
187
|
+
console.log('\nMemory Controller Diagnostics:');
|
|
188
|
+
if (info.cgroupV2) {
|
|
189
|
+
console.log('✓ cgroup v2 detected');
|
|
190
|
+
} else {
|
|
191
|
+
console.log('- cgroup v2 unavailable');
|
|
192
|
+
}
|
|
130
193
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
194
|
+
if (info.memoryController) {
|
|
195
|
+
console.log('✓ Memory controller available');
|
|
196
|
+
} else {
|
|
197
|
+
console.log('- Memory controller unavailable');
|
|
198
|
+
}
|
|
136
199
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
200
|
+
if (info.memoryMaxSupported) {
|
|
201
|
+
console.log('✓ MemoryMax supported');
|
|
202
|
+
} else {
|
|
203
|
+
console.log('- MemoryMax unavailable');
|
|
204
|
+
}
|
|
142
205
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
206
|
+
if (info.memorySwapMaxSupported) {
|
|
207
|
+
console.log('✓ MemorySwapMax supported');
|
|
208
|
+
} else {
|
|
209
|
+
console.log('- MemorySwapMax unavailable');
|
|
210
|
+
}
|
|
148
211
|
|
|
149
|
-
|
|
212
|
+
console.log('');
|
|
150
213
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
214
|
+
if (info.unitDirWritable) {
|
|
215
|
+
console.log('✓ Unit directory writable');
|
|
216
|
+
} else {
|
|
217
|
+
console.log(`! Unit directory not writable (${info.unitDir})`);
|
|
218
|
+
}
|
|
155
219
|
}
|
|
156
220
|
|
|
157
221
|
if (info.lingering) {
|
|
158
222
|
console.log('✓ User lingering is enabled');
|
|
159
223
|
} else {
|
|
160
224
|
console.log('! User lingering is disabled');
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
225
|
+
if (info.linux) {
|
|
226
|
+
console.log('\nUser lingering is disabled.');
|
|
227
|
+
console.log('The service may stop after logout.');
|
|
228
|
+
console.log('\nEnable it manually:');
|
|
229
|
+
console.log(` loginctl enable-linger ${info.username}\n`);
|
|
230
|
+
}
|
|
165
231
|
}
|
|
166
232
|
|
|
233
|
+
console.log('\n✓ Unitup can manage services on this machine');
|
|
167
234
|
return info;
|
|
168
235
|
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all Unitup errors.
|
|
3
|
+
*/
|
|
4
|
+
export class UnitupError extends Error {
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} message
|
|
7
|
+
* @param {string} [code]
|
|
8
|
+
*/
|
|
9
|
+
constructor(message, code = 'UNITUP_ERROR') {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = this.constructor.name;
|
|
12
|
+
this.code = code;
|
|
13
|
+
if (Error.captureStackTrace) {
|
|
14
|
+
Error.captureStackTrace(this, this.constructor);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when an unsupported platform is encountered.
|
|
21
|
+
*/
|
|
22
|
+
export class UnsupportedPlatformError extends UnitupError {
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} [platform]
|
|
25
|
+
*/
|
|
26
|
+
constructor(platform = process.platform) {
|
|
27
|
+
super(`Platform "${platform}" is not supported by Unitup.`, 'ERR_UNSUPPORTED_PLATFORM');
|
|
28
|
+
this.platform = platform;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Thrown when a specified service is not found.
|
|
34
|
+
*/
|
|
35
|
+
export class ServiceNotFoundError extends UnitupError {
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} name
|
|
38
|
+
*/
|
|
39
|
+
constructor(name) {
|
|
40
|
+
super(`Service "${name}" does not exist.`, 'ERR_SERVICE_NOT_FOUND');
|
|
41
|
+
this.serviceName = name;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Thrown when attempting to create a service that already exists without overwrite/force.
|
|
47
|
+
*/
|
|
48
|
+
export class ServiceAlreadyExistsError extends UnitupError {
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} name
|
|
51
|
+
*/
|
|
52
|
+
constructor(name) {
|
|
53
|
+
super(`Service "${name}" already exists. Use --force to overwrite.`, 'ERR_SERVICE_ALREADY_EXISTS');
|
|
54
|
+
this.serviceName = name;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Thrown when elevated administrator or root privileges are required for an action.
|
|
60
|
+
*/
|
|
61
|
+
export class PermissionRequiredError extends UnitupError {
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} message
|
|
64
|
+
* @param {string} [action]
|
|
65
|
+
*/
|
|
66
|
+
constructor(message = 'Administrator/root privileges are required to perform this action.', action) {
|
|
67
|
+
super(message, 'ERR_PERMISSION_REQUIRED');
|
|
68
|
+
this.action = action;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Thrown when a service fails to start.
|
|
74
|
+
*/
|
|
75
|
+
export class ServiceStartError extends UnitupError {
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} name
|
|
78
|
+
* @param {string} [reason]
|
|
79
|
+
*/
|
|
80
|
+
constructor(name, reason) {
|
|
81
|
+
super(`Failed to start service "${name}"${reason ? `: ${reason}` : ''}`, 'ERR_SERVICE_START');
|
|
82
|
+
this.serviceName = name;
|
|
83
|
+
this.reason = reason;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Thrown when a service fails to stop.
|
|
89
|
+
*/
|
|
90
|
+
export class ServiceStopError extends UnitupError {
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} name
|
|
93
|
+
* @param {string} [reason]
|
|
94
|
+
*/
|
|
95
|
+
constructor(name, reason) {
|
|
96
|
+
super(`Failed to stop service "${name}"${reason ? `: ${reason}` : ''}`, 'ERR_SERVICE_STOP');
|
|
97
|
+
this.serviceName = name;
|
|
98
|
+
this.reason = reason;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Thrown when service configuration is invalid.
|
|
104
|
+
*/
|
|
105
|
+
export class InvalidServiceConfigError extends UnitupError {
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} message
|
|
108
|
+
* @param {string} [field]
|
|
109
|
+
*/
|
|
110
|
+
constructor(message, field) {
|
|
111
|
+
super(message, 'ERR_INVALID_CONFIG');
|
|
112
|
+
this.field = field;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Thrown when a required binary executable cannot be resolved.
|
|
118
|
+
*/
|
|
119
|
+
export class ExecutableNotFoundError extends UnitupError {
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} executable
|
|
122
|
+
* @param {string} [message]
|
|
123
|
+
*/
|
|
124
|
+
constructor(executable, message) {
|
|
125
|
+
super(
|
|
126
|
+
message || `Executable "${executable}" could not be found in PATH or at specified path.`,
|
|
127
|
+
'ERR_EXECUTABLE_NOT_FOUND'
|
|
128
|
+
);
|
|
129
|
+
this.executable = executable;
|
|
130
|
+
}
|
|
131
|
+
}
|