jm2 0.1.2 → 0.1.3
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/bin/jm2.js +3 -1
- package/package.json +1 -1
- package/src/cli/commands/install.js +93 -0
- package/src/cli/commands/uninstall.js +95 -0
- package/src/cli/index.js +25 -0
- package/src/core/service.js +664 -0
package/bin/jm2.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* jm2 CLI Entry Point
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* jm2 start Start the daemon
|
|
8
8
|
* jm2 stop Stop the daemon
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* jm2 run <id|name> Run a job manually
|
|
18
18
|
* jm2 logs <id|name> Show job logs
|
|
19
19
|
* jm2 history <id|name> Show job history
|
|
20
|
+
* jm2 install Register JM2 to start on boot
|
|
21
|
+
* jm2 uninstall Unregister JM2 from system startup
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
import { runCli } from '../src/cli/index.js';
|
package/package.json
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 install command
|
|
3
|
+
* Registers JM2 daemon to start on system boot
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
installService,
|
|
8
|
+
getServiceStatus,
|
|
9
|
+
isElevated,
|
|
10
|
+
ServiceType,
|
|
11
|
+
getPlatform,
|
|
12
|
+
isPlatformSupported
|
|
13
|
+
} from '../../core/service.js';
|
|
14
|
+
import { printSuccess, printError, printInfo, printWarning } from '../utils/output.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Execute the install command
|
|
18
|
+
* @param {object} options - Command options
|
|
19
|
+
* @param {boolean} options.user - Install for current user only
|
|
20
|
+
* @param {boolean} options.system - Install system-wide
|
|
21
|
+
* @returns {Promise<number>} Exit code
|
|
22
|
+
*/
|
|
23
|
+
export async function installCommand(options = {}) {
|
|
24
|
+
// Validate platform support
|
|
25
|
+
if (!isPlatformSupported()) {
|
|
26
|
+
printError(`Service installation is not supported on platform: ${getPlatform()}`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Determine installation type
|
|
31
|
+
const type = options.system ? ServiceType.SYSTEM : ServiceType.USER;
|
|
32
|
+
|
|
33
|
+
// Check current status
|
|
34
|
+
const status = getServiceStatus(type);
|
|
35
|
+
if (status.installed) {
|
|
36
|
+
printWarning(`JM2 service is already installed (${type} level).`);
|
|
37
|
+
printInfo(`Service status: ${status.running ? 'running' : 'stopped'}`);
|
|
38
|
+
printInfo('Use "jm2 uninstall" first if you want to reinstall.');
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Show what we're doing
|
|
43
|
+
if (type === ServiceType.SYSTEM) {
|
|
44
|
+
printInfo('Installing JM2 service system-wide...');
|
|
45
|
+
if (!isElevated()) {
|
|
46
|
+
printWarning('Administrator/root privileges required for system-wide installation.');
|
|
47
|
+
printInfo('You will be prompted for elevation if needed.');
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
printInfo('Installing JM2 service for current user...');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Perform installation
|
|
54
|
+
const result = await installService(type);
|
|
55
|
+
|
|
56
|
+
if (result.success) {
|
|
57
|
+
printSuccess(result.message);
|
|
58
|
+
|
|
59
|
+
// Show additional info
|
|
60
|
+
const newStatus = getServiceStatus(type);
|
|
61
|
+
if (newStatus.running) {
|
|
62
|
+
printInfo('Service is now running and will start automatically on boot.');
|
|
63
|
+
} else {
|
|
64
|
+
printWarning('Service installed but not running. You may need to start it manually.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Platform-specific notes
|
|
68
|
+
const platform = getPlatform();
|
|
69
|
+
if (platform === 'darwin') {
|
|
70
|
+
printInfo('macOS: Service configured via launchd');
|
|
71
|
+
} else if (platform === 'linux') {
|
|
72
|
+
printInfo('Linux: Service configured via systemd');
|
|
73
|
+
} else if (platform === 'win32') {
|
|
74
|
+
printInfo('Windows: Service configured via Service Control Manager');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return 0;
|
|
78
|
+
} else {
|
|
79
|
+
printError(result.message);
|
|
80
|
+
|
|
81
|
+
// Provide helpful guidance for common errors
|
|
82
|
+
if (result.message.includes('administrator') || result.message.includes('root') || result.message.includes('privileges')) {
|
|
83
|
+
if (type === ServiceType.SYSTEM) {
|
|
84
|
+
printInfo('Tip: Try installing without --system flag for user-level installation:');
|
|
85
|
+
printInfo(' jm2 install');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default installCommand;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 uninstall command
|
|
3
|
+
* Unregisters JM2 daemon from system startup
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
uninstallService,
|
|
8
|
+
getServiceStatus,
|
|
9
|
+
isElevated,
|
|
10
|
+
ServiceType,
|
|
11
|
+
getPlatform,
|
|
12
|
+
isPlatformSupported
|
|
13
|
+
} from '../../core/service.js';
|
|
14
|
+
import { printSuccess, printError, printInfo, printWarning } from '../utils/output.js';
|
|
15
|
+
import { confirm } from '../utils/prompts.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute the uninstall command
|
|
19
|
+
* @param {object} options - Command options
|
|
20
|
+
* @param {boolean} options.user - Uninstall user-level registration
|
|
21
|
+
* @param {boolean} options.system - Uninstall system-wide registration
|
|
22
|
+
* @param {boolean} options.force - Skip confirmation prompt
|
|
23
|
+
* @returns {Promise<number>} Exit code
|
|
24
|
+
*/
|
|
25
|
+
export async function uninstallCommand(options = {}) {
|
|
26
|
+
// Validate platform support
|
|
27
|
+
if (!isPlatformSupported()) {
|
|
28
|
+
printError(`Service uninstallation is not supported on platform: ${getPlatform()}`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Determine installation type
|
|
33
|
+
const type = options.system ? ServiceType.SYSTEM : ServiceType.USER;
|
|
34
|
+
|
|
35
|
+
// Check current status
|
|
36
|
+
const status = getServiceStatus(type);
|
|
37
|
+
if (!status.installed) {
|
|
38
|
+
// Check if installed at the other level
|
|
39
|
+
const otherType = type === ServiceType.USER ? ServiceType.SYSTEM : ServiceType.USER;
|
|
40
|
+
const otherStatus = getServiceStatus(otherType);
|
|
41
|
+
|
|
42
|
+
if (otherStatus.installed) {
|
|
43
|
+
printWarning(`JM2 service is not installed at ${type} level, but is installed at ${otherType} level.`);
|
|
44
|
+
printInfo(`Use "jm2 uninstall --${otherType}" to remove it.`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
printInfo(`JM2 service is not installed (${type} level).`);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Show what we're doing
|
|
53
|
+
if (type === ServiceType.SYSTEM) {
|
|
54
|
+
printInfo('Uninstalling JM2 service system-wide...');
|
|
55
|
+
if (!isElevated()) {
|
|
56
|
+
printWarning('Administrator/root privileges required for system-wide uninstallation.');
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
printInfo('Uninstalling JM2 service for current user...');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Confirm if not forced
|
|
63
|
+
if (!options.force) {
|
|
64
|
+
const confirmed = await confirm(
|
|
65
|
+
`Are you sure you want to uninstall the JM2 service (${type} level)?`
|
|
66
|
+
);
|
|
67
|
+
if (!confirmed) {
|
|
68
|
+
printInfo('Uninstall cancelled.');
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Perform uninstallation
|
|
74
|
+
const result = await uninstallService(type);
|
|
75
|
+
|
|
76
|
+
if (result.success) {
|
|
77
|
+
printSuccess(result.message);
|
|
78
|
+
printInfo('JM2 will no longer start automatically on boot.');
|
|
79
|
+
return 0;
|
|
80
|
+
} else {
|
|
81
|
+
printError(result.message);
|
|
82
|
+
|
|
83
|
+
// Provide helpful guidance for common errors
|
|
84
|
+
if (result.message.includes('administrator') || result.message.includes('root') || result.message.includes('privileges')) {
|
|
85
|
+
if (type === ServiceType.SYSTEM) {
|
|
86
|
+
printInfo('Tip: If you want to uninstall the user-level service instead:');
|
|
87
|
+
printInfo(' jm2 uninstall');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default uninstallCommand;
|
package/src/cli/index.js
CHANGED
|
@@ -26,6 +26,8 @@ import { historyCommand } from './commands/history.js';
|
|
|
26
26
|
import { flushCommand } from './commands/flush.js';
|
|
27
27
|
import { exportCommand } from './commands/export.js';
|
|
28
28
|
import { importCommand } from './commands/import.js';
|
|
29
|
+
import { installCommand } from './commands/install.js';
|
|
30
|
+
import { uninstallCommand } from './commands/uninstall.js';
|
|
29
31
|
|
|
30
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
33
|
const __dirname = dirname(__filename);
|
|
@@ -271,6 +273,29 @@ export async function runCli() {
|
|
|
271
273
|
process.exit(exitCode);
|
|
272
274
|
});
|
|
273
275
|
|
|
276
|
+
// Install command
|
|
277
|
+
program
|
|
278
|
+
.command('install')
|
|
279
|
+
.description('Register JM2 daemon to start on system boot')
|
|
280
|
+
.option('--user', 'Install for current user only (default)', false)
|
|
281
|
+
.option('--system', 'Install system-wide (requires admin/root)', false)
|
|
282
|
+
.action(async (options) => {
|
|
283
|
+
const exitCode = await installCommand(options);
|
|
284
|
+
process.exit(exitCode);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Uninstall command
|
|
288
|
+
program
|
|
289
|
+
.command('uninstall')
|
|
290
|
+
.description('Unregister JM2 daemon from system startup')
|
|
291
|
+
.option('--user', 'Uninstall user-level registration (default)', false)
|
|
292
|
+
.option('--system', 'Uninstall system-wide registration (requires admin/root)', false)
|
|
293
|
+
.option('-f, --force', 'Skip confirmation prompt', false)
|
|
294
|
+
.action(async (options) => {
|
|
295
|
+
const exitCode = await uninstallCommand(options);
|
|
296
|
+
process.exit(exitCode);
|
|
297
|
+
});
|
|
298
|
+
|
|
274
299
|
// Parse command line arguments
|
|
275
300
|
await program.parseAsync();
|
|
276
301
|
}
|
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service management for JM2
|
|
3
|
+
* Handles install/uninstall of system service for auto-start on boot
|
|
4
|
+
* Supports macOS (launchd), Linux (systemd), and Windows (service)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import {
|
|
11
|
+
writeFileSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
existsSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
constants
|
|
17
|
+
} from 'node:fs';
|
|
18
|
+
import { spawn, execSync } from 'node:child_process';
|
|
19
|
+
import { getDataDir, getLogsDir } from '../utils/paths.js';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Service registration types
|
|
26
|
+
*/
|
|
27
|
+
export const ServiceType = {
|
|
28
|
+
USER: 'user',
|
|
29
|
+
SYSTEM: 'system'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Supported platforms
|
|
34
|
+
*/
|
|
35
|
+
export const Platform = {
|
|
36
|
+
DARWIN: 'darwin',
|
|
37
|
+
LINUX: 'linux',
|
|
38
|
+
WIN32: 'win32'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if running with elevated privileges (root/admin)
|
|
43
|
+
* @returns {boolean} True if running as admin/root
|
|
44
|
+
*/
|
|
45
|
+
export function isElevated() {
|
|
46
|
+
if (process.platform === 'win32') {
|
|
47
|
+
// On Windows, check if we have admin privileges
|
|
48
|
+
try {
|
|
49
|
+
// Try to access a protected resource
|
|
50
|
+
execSync('net session', { stdio: 'ignore' });
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
// On Unix systems, check uid
|
|
57
|
+
return process.getuid ? process.getuid() === 0 : false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the platform-specific service manager
|
|
63
|
+
* @returns {PlatformService} Platform service implementation
|
|
64
|
+
*/
|
|
65
|
+
function getPlatformService() {
|
|
66
|
+
const platform = process.platform;
|
|
67
|
+
|
|
68
|
+
switch (platform) {
|
|
69
|
+
case Platform.DARWIN:
|
|
70
|
+
return new DarwinService();
|
|
71
|
+
case Platform.LINUX:
|
|
72
|
+
return new LinuxService();
|
|
73
|
+
case Platform.WIN32:
|
|
74
|
+
return new WindowsService();
|
|
75
|
+
default:
|
|
76
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get path to the jm2 executable
|
|
82
|
+
* @returns {string} Path to jm2 binary
|
|
83
|
+
*/
|
|
84
|
+
function getJm2Path() {
|
|
85
|
+
// Use the bin/jm2.js entry point
|
|
86
|
+
const pkgPath = join(__dirname, '../../package.json');
|
|
87
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
88
|
+
|
|
89
|
+
if (pkg.bin && pkg.bin.jm2) {
|
|
90
|
+
return join(__dirname, '../..', pkg.bin.jm2);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return join(__dirname, '../../bin/jm2.js');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get Node.js executable path
|
|
98
|
+
* @returns {string} Path to node executable
|
|
99
|
+
*/
|
|
100
|
+
function getNodePath() {
|
|
101
|
+
return process.execPath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Base class for platform-specific service management
|
|
106
|
+
*/
|
|
107
|
+
class PlatformService {
|
|
108
|
+
/**
|
|
109
|
+
* Install the service
|
|
110
|
+
* @param {string} type - Service type (user or system)
|
|
111
|
+
* @returns {Promise<{success: boolean, message: string}>}
|
|
112
|
+
*/
|
|
113
|
+
async install(type) {
|
|
114
|
+
throw new Error('Not implemented');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Uninstall the service
|
|
119
|
+
* @param {string} type - Service type (user or system)
|
|
120
|
+
* @returns {Promise<{success: boolean, message: string}>}
|
|
121
|
+
*/
|
|
122
|
+
async uninstall(type) {
|
|
123
|
+
throw new Error('Not implemented');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if service is installed
|
|
128
|
+
* @param {string} type - Service type (user or system)
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
isInstalled(type) {
|
|
132
|
+
throw new Error('Not implemented');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get service status
|
|
137
|
+
* @param {string} type - Service type (user or system)
|
|
138
|
+
* @returns {{installed: boolean, running: boolean}}
|
|
139
|
+
*/
|
|
140
|
+
getStatus(type) {
|
|
141
|
+
throw new Error('Not implemented');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* macOS launchd service implementation
|
|
147
|
+
*/
|
|
148
|
+
class DarwinService extends PlatformService {
|
|
149
|
+
getPlistPath(type) {
|
|
150
|
+
if (type === ServiceType.USER) {
|
|
151
|
+
return join(homedir(), 'Library', 'LaunchAgents', 'com.jm2.daemon.plist');
|
|
152
|
+
} else {
|
|
153
|
+
return '/Library/LaunchDaemons/com.jm2.daemon.plist';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
generatePlist(type) {
|
|
158
|
+
const jm2Path = getJm2Path();
|
|
159
|
+
const nodePath = getNodePath();
|
|
160
|
+
const dataDir = getDataDir();
|
|
161
|
+
const logDir = getLogsDir();
|
|
162
|
+
|
|
163
|
+
const label = 'com.jm2.daemon';
|
|
164
|
+
const stdoutPath = join(logDir, 'service-out.log');
|
|
165
|
+
const stderrPath = join(logDir, 'service-err.log');
|
|
166
|
+
|
|
167
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
168
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
169
|
+
<plist version="1.0">
|
|
170
|
+
<dict>
|
|
171
|
+
<key>Label</key>
|
|
172
|
+
<string>${label}</string>
|
|
173
|
+
<key>ProgramArguments</key>
|
|
174
|
+
<array>
|
|
175
|
+
<string>${nodePath}</string>
|
|
176
|
+
<string>${jm2Path}</string>
|
|
177
|
+
<string>start</string>
|
|
178
|
+
</array>
|
|
179
|
+
<key>RunAtLoad</key>
|
|
180
|
+
<true/>
|
|
181
|
+
<key>KeepAlive</key>
|
|
182
|
+
<true/>
|
|
183
|
+
<key>StandardOutPath</key>
|
|
184
|
+
<string>${stdoutPath}</string>
|
|
185
|
+
<key>StandardErrorPath</key>
|
|
186
|
+
<string>${stderrPath}</string>
|
|
187
|
+
<key>EnvironmentVariables</key>
|
|
188
|
+
<dict>
|
|
189
|
+
<key>JM2_DATA_DIR</key>
|
|
190
|
+
<string>${dataDir}</string>
|
|
191
|
+
</dict>
|
|
192
|
+
</dict>
|
|
193
|
+
</plist>`;
|
|
194
|
+
|
|
195
|
+
return plist;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async install(type) {
|
|
199
|
+
const plistPath = this.getPlistPath(type);
|
|
200
|
+
|
|
201
|
+
// Check if already installed
|
|
202
|
+
if (existsSync(plistPath)) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
message: `Service is already installed at ${plistPath}. Use uninstall first.`
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check permissions for system install
|
|
210
|
+
if (type === ServiceType.SYSTEM && !isElevated()) {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
message: 'System-wide installation requires administrator privileges. Run with sudo.'
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// Ensure directory exists
|
|
219
|
+
const dir = dirname(plistPath);
|
|
220
|
+
if (!existsSync(dir)) {
|
|
221
|
+
mkdirSync(dir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Write plist file
|
|
225
|
+
const plist = this.generatePlist(type);
|
|
226
|
+
writeFileSync(plistPath, plist, 'utf8');
|
|
227
|
+
|
|
228
|
+
// Load the service
|
|
229
|
+
if (type === ServiceType.USER) {
|
|
230
|
+
execSync(`launchctl load ${plistPath}`, { stdio: 'inherit' });
|
|
231
|
+
execSync('launchctl start com.jm2.daemon', { stdio: 'inherit' });
|
|
232
|
+
} else {
|
|
233
|
+
execSync(`sudo launchctl load ${plistPath}`, { stdio: 'inherit' });
|
|
234
|
+
execSync('sudo launchctl start com.jm2.daemon', { stdio: 'inherit' });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
success: true,
|
|
239
|
+
message: `JM2 service installed successfully for ${type === ServiceType.USER ? 'current user' : 'system-wide'}.`
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
success: false,
|
|
244
|
+
message: `Failed to install service: ${error.message}`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async uninstall(type) {
|
|
250
|
+
const plistPath = this.getPlistPath(type);
|
|
251
|
+
|
|
252
|
+
// Check if installed
|
|
253
|
+
if (!existsSync(plistPath)) {
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
message: `Service is not installed (${type} level).`
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check permissions for system uninstall
|
|
261
|
+
if (type === ServiceType.SYSTEM && !isElevated()) {
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
message: 'System-wide uninstallation requires administrator privileges. Run with sudo.'
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// Unload the service
|
|
270
|
+
if (type === ServiceType.USER) {
|
|
271
|
+
execSync('launchctl stop com.jm2.daemon 2>/dev/null || true', { stdio: 'ignore' });
|
|
272
|
+
execSync(`launchctl unload ${plistPath} 2>/dev/null || true`, { stdio: 'ignore' });
|
|
273
|
+
} else {
|
|
274
|
+
execSync('sudo launchctl stop com.jm2.daemon 2>/dev/null || true', { stdio: 'ignore' });
|
|
275
|
+
execSync(`sudo launchctl unload ${plistPath} 2>/dev/null || true`, { stdio: 'ignore' });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Remove plist file
|
|
279
|
+
unlinkSync(plistPath);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
success: true,
|
|
283
|
+
message: `JM2 service uninstalled successfully from ${type} level.`
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
message: `Failed to uninstall service: ${error.message}`
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
isInstalled(type) {
|
|
294
|
+
const plistPath = this.getPlistPath(type);
|
|
295
|
+
return existsSync(plistPath);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
getStatus(type) {
|
|
299
|
+
const installed = this.isInstalled(type);
|
|
300
|
+
let running = false;
|
|
301
|
+
|
|
302
|
+
if (installed) {
|
|
303
|
+
try {
|
|
304
|
+
const result = execSync('launchctl list | grep com.jm2.daemon', {
|
|
305
|
+
encoding: 'utf8',
|
|
306
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
307
|
+
});
|
|
308
|
+
running = result.includes('com.jm2.daemon');
|
|
309
|
+
} catch {
|
|
310
|
+
running = false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { installed, running };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Linux systemd service implementation
|
|
320
|
+
*/
|
|
321
|
+
class LinuxService extends PlatformService {
|
|
322
|
+
getServicePath(type) {
|
|
323
|
+
if (type === ServiceType.USER) {
|
|
324
|
+
return join(homedir(), '.config', 'systemd', 'user', 'jm2.service');
|
|
325
|
+
} else {
|
|
326
|
+
return '/etc/systemd/system/jm2.service';
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
generateService(type) {
|
|
331
|
+
const jm2Path = getJm2Path();
|
|
332
|
+
const nodePath = getNodePath();
|
|
333
|
+
const dataDir = getDataDir();
|
|
334
|
+
const logDir = getLogsDir();
|
|
335
|
+
|
|
336
|
+
const service = `[Unit]
|
|
337
|
+
Description=JM2 Job Manager Daemon
|
|
338
|
+
After=network.target
|
|
339
|
+
|
|
340
|
+
[Service]
|
|
341
|
+
Type=forking
|
|
342
|
+
ExecStart=${nodePath} ${jm2Path} start
|
|
343
|
+
ExecStop=${nodePath} ${jm2Path} stop
|
|
344
|
+
ExecReload=${nodePath} ${jm2Path} restart
|
|
345
|
+
Restart=always
|
|
346
|
+
RestartSec=10
|
|
347
|
+
Environment="JM2_DATA_DIR=${dataDir}"
|
|
348
|
+
StandardOutput=append:${join(logDir, 'service-out.log')}
|
|
349
|
+
StandardError=append:${join(logDir, 'service-err.log')}
|
|
350
|
+
|
|
351
|
+
[Install]
|
|
352
|
+
WantedBy=${type === ServiceType.USER ? 'default.target' : 'multi-user.target'}
|
|
353
|
+
`;
|
|
354
|
+
|
|
355
|
+
return service;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async install(type) {
|
|
359
|
+
const servicePath = this.getServicePath(type);
|
|
360
|
+
|
|
361
|
+
// Check if already installed
|
|
362
|
+
if (existsSync(servicePath)) {
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
message: `Service is already installed at ${servicePath}. Use uninstall first.`
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check permissions for system install
|
|
370
|
+
if (type === ServiceType.SYSTEM && !isElevated()) {
|
|
371
|
+
return {
|
|
372
|
+
success: false,
|
|
373
|
+
message: 'System-wide installation requires root privileges. Run with sudo.'
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
// Ensure directory exists
|
|
379
|
+
const dir = dirname(servicePath);
|
|
380
|
+
if (!existsSync(dir)) {
|
|
381
|
+
mkdirSync(dir, { recursive: true });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Write service file
|
|
385
|
+
const service = this.generateService(type);
|
|
386
|
+
writeFileSync(servicePath, service, 'utf8');
|
|
387
|
+
|
|
388
|
+
// Reload systemd
|
|
389
|
+
if (type === ServiceType.USER) {
|
|
390
|
+
execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
|
|
391
|
+
execSync('systemctl --user enable jm2', { stdio: 'inherit' });
|
|
392
|
+
execSync('systemctl --user start jm2', { stdio: 'inherit' });
|
|
393
|
+
} else {
|
|
394
|
+
execSync('systemctl daemon-reload', { stdio: 'inherit' });
|
|
395
|
+
execSync('systemctl enable jm2', { stdio: 'inherit' });
|
|
396
|
+
execSync('systemctl start jm2', { stdio: 'inherit' });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
success: true,
|
|
401
|
+
message: `JM2 service installed successfully for ${type === ServiceType.USER ? 'current user' : 'system-wide'}.`
|
|
402
|
+
};
|
|
403
|
+
} catch (error) {
|
|
404
|
+
return {
|
|
405
|
+
success: false,
|
|
406
|
+
message: `Failed to install service: ${error.message}`
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async uninstall(type) {
|
|
412
|
+
const servicePath = this.getServicePath(type);
|
|
413
|
+
|
|
414
|
+
// Check if installed
|
|
415
|
+
if (!existsSync(servicePath)) {
|
|
416
|
+
return {
|
|
417
|
+
success: false,
|
|
418
|
+
message: `Service is not installed (${type} level).`
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Check permissions for system uninstall
|
|
423
|
+
if (type === ServiceType.SYSTEM && !isElevated()) {
|
|
424
|
+
return {
|
|
425
|
+
success: false,
|
|
426
|
+
message: 'System-wide uninstallation requires root privileges. Run with sudo.'
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
// Stop and disable the service
|
|
432
|
+
if (type === ServiceType.USER) {
|
|
433
|
+
execSync('systemctl --user stop jm2 2>/dev/null || true', { stdio: 'ignore' });
|
|
434
|
+
execSync('systemctl --user disable jm2 2>/dev/null || true', { stdio: 'ignore' });
|
|
435
|
+
} else {
|
|
436
|
+
execSync('systemctl stop jm2 2>/dev/null || true', { stdio: 'ignore' });
|
|
437
|
+
execSync('systemctl disable jm2 2>/dev/null || true', { stdio: 'ignore' });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Remove service file
|
|
441
|
+
unlinkSync(servicePath);
|
|
442
|
+
|
|
443
|
+
// Reload systemd
|
|
444
|
+
if (type === ServiceType.USER) {
|
|
445
|
+
execSync('systemctl --user daemon-reload', { stdio: 'ignore' });
|
|
446
|
+
} else {
|
|
447
|
+
execSync('systemctl daemon-reload', { stdio: 'ignore' });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
success: true,
|
|
452
|
+
message: `JM2 service uninstalled successfully from ${type} level.`
|
|
453
|
+
};
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return {
|
|
456
|
+
success: false,
|
|
457
|
+
message: `Failed to uninstall service: ${error.message}`
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
isInstalled(type) {
|
|
463
|
+
const servicePath = this.getServicePath(type);
|
|
464
|
+
return existsSync(servicePath);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
getStatus(type) {
|
|
468
|
+
const installed = this.isInstalled(type);
|
|
469
|
+
let running = false;
|
|
470
|
+
|
|
471
|
+
if (installed) {
|
|
472
|
+
try {
|
|
473
|
+
const result = execSync(
|
|
474
|
+
type === ServiceType.USER ? 'systemctl --user is-active jm2' : 'systemctl is-active jm2',
|
|
475
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
|
|
476
|
+
);
|
|
477
|
+
running = result.trim() === 'active';
|
|
478
|
+
} catch {
|
|
479
|
+
running = false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return { installed, running };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Windows service implementation
|
|
489
|
+
* Uses a simple batch wrapper approach for now
|
|
490
|
+
*/
|
|
491
|
+
class WindowsService extends PlatformService {
|
|
492
|
+
getServiceName() {
|
|
493
|
+
return 'JM2Daemon';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
getWrapperPath() {
|
|
497
|
+
return join(getDataDir(), 'jm2-service.bat');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
generateWrapper() {
|
|
501
|
+
const jm2Path = getJm2Path();
|
|
502
|
+
const nodePath = getNodePath();
|
|
503
|
+
const dataDir = getDataDir();
|
|
504
|
+
|
|
505
|
+
const wrapper = `@echo off
|
|
506
|
+
set JM2_DATA_DIR=${dataDir}
|
|
507
|
+
"${nodePath}" "${jm2Path}" start
|
|
508
|
+
`;
|
|
509
|
+
|
|
510
|
+
return wrapper;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async install(type) {
|
|
514
|
+
// Windows services require admin privileges
|
|
515
|
+
if (!isElevated()) {
|
|
516
|
+
return {
|
|
517
|
+
success: false,
|
|
518
|
+
message: 'Windows service installation requires administrator privileges. Run as Administrator.'
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
// Create wrapper script
|
|
524
|
+
const wrapperPath = this.getWrapperPath();
|
|
525
|
+
const wrapper = this.generateWrapper();
|
|
526
|
+
writeFileSync(wrapperPath, wrapper, 'utf8');
|
|
527
|
+
|
|
528
|
+
// Install service using sc.exe
|
|
529
|
+
const serviceName = this.getServiceName();
|
|
530
|
+
const binPath = `cmd.exe /c "${wrapperPath}"`;
|
|
531
|
+
|
|
532
|
+
execSync(`sc create "${serviceName}" binPath= "${binPath}" start= auto`, {
|
|
533
|
+
stdio: 'inherit'
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Start the service
|
|
537
|
+
execSync(`sc start "${serviceName}"`, { stdio: 'inherit' });
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
success: true,
|
|
541
|
+
message: 'JM2 service installed successfully.'
|
|
542
|
+
};
|
|
543
|
+
} catch (error) {
|
|
544
|
+
return {
|
|
545
|
+
success: false,
|
|
546
|
+
message: `Failed to install service: ${error.message}`
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async uninstall(type) {
|
|
552
|
+
// Windows service removal requires admin privileges
|
|
553
|
+
if (!isElevated()) {
|
|
554
|
+
return {
|
|
555
|
+
success: false,
|
|
556
|
+
message: 'Windows service uninstallation requires administrator privileges. Run as Administrator.'
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
const serviceName = this.getServiceName();
|
|
562
|
+
|
|
563
|
+
// Stop the service first
|
|
564
|
+
try {
|
|
565
|
+
execSync(`sc stop "${serviceName}"`, { stdio: 'ignore' });
|
|
566
|
+
} catch {
|
|
567
|
+
// Service might not be running
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Delete the service
|
|
571
|
+
execSync(`sc delete "${serviceName}"`, { stdio: 'inherit' });
|
|
572
|
+
|
|
573
|
+
// Remove wrapper script
|
|
574
|
+
const wrapperPath = this.getWrapperPath();
|
|
575
|
+
if (existsSync(wrapperPath)) {
|
|
576
|
+
unlinkSync(wrapperPath);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
success: true,
|
|
581
|
+
message: 'JM2 service uninstalled successfully.'
|
|
582
|
+
};
|
|
583
|
+
} catch (error) {
|
|
584
|
+
return {
|
|
585
|
+
success: false,
|
|
586
|
+
message: `Failed to uninstall service: ${error.message}`
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
isInstalled(type) {
|
|
592
|
+
try {
|
|
593
|
+
const serviceName = this.getServiceName();
|
|
594
|
+
execSync(`sc query "${serviceName}"`, { stdio: 'ignore' });
|
|
595
|
+
return true;
|
|
596
|
+
} catch {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
getStatus(type) {
|
|
602
|
+
const installed = this.isInstalled(type);
|
|
603
|
+
let running = false;
|
|
604
|
+
|
|
605
|
+
if (installed) {
|
|
606
|
+
try {
|
|
607
|
+
const serviceName = this.getServiceName();
|
|
608
|
+
const result = execSync(`sc query "${serviceName}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
609
|
+
running = result.includes('RUNNING');
|
|
610
|
+
} catch {
|
|
611
|
+
running = false;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return { installed, running };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Install JM2 as a system service
|
|
621
|
+
* @param {string} type - Service type: 'user' or 'system'
|
|
622
|
+
* @returns {Promise<{success: boolean, message: string}>}
|
|
623
|
+
*/
|
|
624
|
+
export async function installService(type = ServiceType.USER) {
|
|
625
|
+
const service = getPlatformService();
|
|
626
|
+
return service.install(type);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Uninstall JM2 system service
|
|
631
|
+
* @param {string} type - Service type: 'user' or 'system'
|
|
632
|
+
* @returns {Promise<{success: boolean, message: string}>}
|
|
633
|
+
*/
|
|
634
|
+
export async function uninstallService(type = ServiceType.USER) {
|
|
635
|
+
const service = getPlatformService();
|
|
636
|
+
return service.uninstall(type);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Check if service is installed
|
|
641
|
+
* @param {string} type - Service type: 'user' or 'system'
|
|
642
|
+
* @returns {{installed: boolean, running: boolean}}
|
|
643
|
+
*/
|
|
644
|
+
export function getServiceStatus(type = ServiceType.USER) {
|
|
645
|
+
const service = getPlatformService();
|
|
646
|
+
return service.getStatus(type);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Get current platform
|
|
651
|
+
* @returns {string} Platform identifier
|
|
652
|
+
*/
|
|
653
|
+
export function getPlatform() {
|
|
654
|
+
return process.platform;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Check if platform is supported for service installation
|
|
659
|
+
* @returns {boolean}
|
|
660
|
+
*/
|
|
661
|
+
export function isPlatformSupported() {
|
|
662
|
+
const platform = process.platform;
|
|
663
|
+
return Object.values(Platform).includes(platform);
|
|
664
|
+
}
|