@flun/windows 3.0.11 → 4.0.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 +36 -15
- package/LICENSE +34 -34
- package/README.md +248 -140
- package/bin/cli.js +218 -0
- package/copy-files.js +34 -34
- package/index.d.ts +133 -133
- package/lib/binaries.js +133 -133
- package/lib/daemon.js +66 -65
- package/package.json +6 -2
- package/win-service.config.js +68 -0
- package/sevWin.js +0 -18
package/lib/binaries.js
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
import { path, fs, exec, getDirname } from './shared.js';
|
|
2
|
-
/**
|
|
3
|
-
* 从 Node.js `os` 模块导入的 `platform` 函数,用于获取操作系统平台字符串;
|
|
4
|
-
* >查看定义:@see {@link platform}
|
|
5
|
-
* @type {() => string}
|
|
6
|
-
*/
|
|
7
|
-
import { platform, tmpdir } from 'os';
|
|
8
|
-
|
|
9
|
-
const __dirname = getDirname(import.meta.url), vbsPath = path.resolve(__dirname, '../bin/elevate.vbs'),
|
|
10
|
-
/** 参数处理函数,用于标准化 options 和 callback 参数 */
|
|
11
|
-
_params = (options = {}, callback) => {
|
|
12
|
-
callback = callback || function () { };
|
|
13
|
-
if (typeof options === 'function') callback = options, options = {};
|
|
14
|
-
if (typeof options !== 'object') throw '参数 options 无效;';
|
|
15
|
-
return { options, callback };
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
// 文件读取
|
|
19
|
-
readFileSafe = (Path, value = '') => {
|
|
20
|
-
try {
|
|
21
|
-
return fs.readFileSync(Path, 'utf8');
|
|
22
|
-
} catch (e) { return value; }
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 提升当前进程权限(Windows UAC)
|
|
27
|
-
* >查看定义:@see {@link elevate}
|
|
28
|
-
* @param {string} cmd - 要使用提升权限执行的命令
|
|
29
|
-
* @param {Object} [options] - 传递给 child_process.exec 的选项
|
|
30
|
-
* @param {string} [options.cwd] - 工作目录
|
|
31
|
-
* @param {number} [options.timeout] - 超时时间(毫秒)
|
|
32
|
-
* @param {(error: Error|null, stdout: string, stderr: string) => void} [callback] - 执行完成后的回调函数
|
|
33
|
-
* @returns {void}
|
|
34
|
-
* @example
|
|
35
|
-
* import { elevate } from '@flun/windows';
|
|
36
|
-
*
|
|
37
|
-
* // 基本用法
|
|
38
|
-
* elevate('echo "Hello World" && whoami',{}, (error, stdout, stderr) => {
|
|
39
|
-
* if (error) {
|
|
40
|
-
* console.error('执行失败:', error);
|
|
41
|
-
* } else {
|
|
42
|
-
* console.log('执行成功,输出:', stdout);
|
|
43
|
-
* console.log('错误输出:', stderr);
|
|
44
|
-
* }
|
|
45
|
-
* });
|
|
46
|
-
*
|
|
47
|
-
* // 带选项参数
|
|
48
|
-
* elevate('echo "Hello World" && whoami', { cwd:'C:\\' }, callback);
|
|
49
|
-
*
|
|
50
|
-
* // 多种参数组合
|
|
51
|
-
* elevate('echo "Hello World" && whoami', (err, stdout)=> { }); // options视为回调
|
|
52
|
-
* elevate('echo "Hello World" && whoami'); // 无回调
|
|
53
|
-
*/
|
|
54
|
-
const elevate = (cmd, options, callback) => {
|
|
55
|
-
const p = _params(options, callback), tmpDir = tmpdir(),
|
|
56
|
-
[vbsFile, outFile, batFile, cmdFile] = ['flun_el.vbs', 'flun_el_output.txt', 'flun_el.bat', 'flun_el.cmd']
|
|
57
|
-
.map(name => path.join(tmpDir, name)),
|
|
58
|
-
escapedCmd = cmd.replace(/"/g, '""').replace(/%/g, "%%"),
|
|
59
|
-
vbs = readFileSafe(vbsPath).replace(/{command}/g, escapedCmd);
|
|
60
|
-
|
|
61
|
-
// 写入VBS文件
|
|
62
|
-
try {
|
|
63
|
-
fs.writeFileSync(vbsFile, vbs, 'utf8');
|
|
64
|
-
} catch (writeError) {
|
|
65
|
-
return p.callback(writeError, '', '');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
exec(`wscript.exe "${vbsFile}"`, { timeout: 20000 }, error => {
|
|
69
|
-
setTimeout(() => {
|
|
70
|
-
const output = readFileSafe(outFile).replace(/\r\n$|\n$/, '').trim();
|
|
71
|
-
|
|
72
|
-
// 清理临时文件
|
|
73
|
-
[batFile, cmdFile, outFile, vbsFile].forEach(f => {
|
|
74
|
-
try { fs.unlinkSync(f) } catch (e) { }
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (error) return p.callback(error, '', '');
|
|
78
|
-
p.callback(null, output, '');
|
|
79
|
-
}, 1600);
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 使用 sudo 方式提升权限(与 elevate 相似但体验更好)
|
|
85
|
-
* >查看定义:@see {@link sudo}
|
|
86
|
-
* @param {string} cmd - 要使用提升权限执行的命令
|
|
87
|
-
* @param {Object} [options] - 传递给 child_process.exec 的选项
|
|
88
|
-
* @param {string} [options.cwd] - 工作目录
|
|
89
|
-
* @param {number} [options.timeout] - 超时时间(毫秒)
|
|
90
|
-
* @param {(error: Error|null, stdout: string, stderr: string) => void} [callback] - 执行完成后的回调函数
|
|
91
|
-
* @returns {void}
|
|
92
|
-
* @example
|
|
93
|
-
* import { sudo } from '@flun/windows';
|
|
94
|
-
*
|
|
95
|
-
* // 基本用法 - 命令、选项和回调
|
|
96
|
-
* sudo('echo "Hello World" && whoami', {}, (error, stdout, stderr) => {
|
|
97
|
-
* if (error) {
|
|
98
|
-
* console.error('执行失败:', error);
|
|
99
|
-
* } else {
|
|
100
|
-
* console.log('执行成功,输出:', stdout);
|
|
101
|
-
* console.log('错误输出:', stderr);
|
|
102
|
-
* }
|
|
103
|
-
* });
|
|
104
|
-
*
|
|
105
|
-
* // 带选项参数
|
|
106
|
-
* sudo('echo "Hello World" && whoami', {cwd:'C:\\'}, callback);
|
|
107
|
-
*
|
|
108
|
-
* sudo('echo "Hello World" && whoami' ); // 无回调
|
|
109
|
-
*/
|
|
110
|
-
const sudo = (cmd, options, callback) => {
|
|
111
|
-
const p = _params(options, callback);
|
|
112
|
-
exec(`sudo ${cmd}`, p.options, p.callback);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 检查当前用户是否拥有管理员权限
|
|
117
|
-
* >查看定义:@see {@link isAdminUser}
|
|
118
|
-
* @param {(isAdmin: boolean) => void} callback - 回调函数,接收布尔值表示是否为管理员
|
|
119
|
-
* @returns {void}
|
|
120
|
-
* @example
|
|
121
|
-
* import { isAdminUser } from '@flun/windows';
|
|
122
|
-
* isAdminUser(isAdmin => {
|
|
123
|
-
* if (isAdmin) console.log('当前用户是管理员');
|
|
124
|
-
* else console.log('当前用户不是管理员');
|
|
125
|
-
* });
|
|
126
|
-
*/
|
|
127
|
-
const isAdminUser = callback => {
|
|
128
|
-
exec('NET SESSION', (err, so, se) => {
|
|
129
|
-
if (se.length !== 0) elevate('NET SESSION', (_err, _so, _se) => callback(_se.length === 0));
|
|
130
|
-
else callback(true);
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
|
|
1
|
+
import { path, fs, exec, getDirname } from './shared.js';
|
|
2
|
+
/**
|
|
3
|
+
* 从 Node.js `os` 模块导入的 `platform` 函数,用于获取操作系统平台字符串;
|
|
4
|
+
* >查看定义:@see {@link platform}
|
|
5
|
+
* @type {() => string}
|
|
6
|
+
*/
|
|
7
|
+
import { platform, tmpdir } from 'os';
|
|
8
|
+
|
|
9
|
+
const __dirname = getDirname(import.meta.url), vbsPath = path.resolve(__dirname, '../bin/elevate.vbs'),
|
|
10
|
+
/** 参数处理函数,用于标准化 options 和 callback 参数 */
|
|
11
|
+
_params = (options = {}, callback) => {
|
|
12
|
+
callback = callback || function () { };
|
|
13
|
+
if (typeof options === 'function') callback = options, options = {};
|
|
14
|
+
if (typeof options !== 'object') throw '参数 options 无效;';
|
|
15
|
+
return { options, callback };
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
// 文件读取
|
|
19
|
+
readFileSafe = (Path, value = '') => {
|
|
20
|
+
try {
|
|
21
|
+
return fs.readFileSync(Path, 'utf8');
|
|
22
|
+
} catch (e) { return value; }
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 提升当前进程权限(Windows UAC)
|
|
27
|
+
* >查看定义:@see {@link elevate}
|
|
28
|
+
* @param {string} cmd - 要使用提升权限执行的命令
|
|
29
|
+
* @param {Object} [options] - 传递给 child_process.exec 的选项
|
|
30
|
+
* @param {string} [options.cwd] - 工作目录
|
|
31
|
+
* @param {number} [options.timeout] - 超时时间(毫秒)
|
|
32
|
+
* @param {(error: Error|null, stdout: string, stderr: string) => void} [callback] - 执行完成后的回调函数
|
|
33
|
+
* @returns {void}
|
|
34
|
+
* @example
|
|
35
|
+
* import { elevate } from '@flun/windows';
|
|
36
|
+
*
|
|
37
|
+
* // 基本用法
|
|
38
|
+
* elevate('echo "Hello World" && whoami',{}, (error, stdout, stderr) => {
|
|
39
|
+
* if (error) {
|
|
40
|
+
* console.error('执行失败:', error);
|
|
41
|
+
* } else {
|
|
42
|
+
* console.log('执行成功,输出:', stdout);
|
|
43
|
+
* console.log('错误输出:', stderr);
|
|
44
|
+
* }
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // 带选项参数
|
|
48
|
+
* elevate('echo "Hello World" && whoami', { cwd:'C:\\' }, callback);
|
|
49
|
+
*
|
|
50
|
+
* // 多种参数组合
|
|
51
|
+
* elevate('echo "Hello World" && whoami', (err, stdout)=> { }); // options视为回调
|
|
52
|
+
* elevate('echo "Hello World" && whoami'); // 无回调
|
|
53
|
+
*/
|
|
54
|
+
const elevate = (cmd, options, callback) => {
|
|
55
|
+
const p = _params(options, callback), tmpDir = tmpdir(),
|
|
56
|
+
[vbsFile, outFile, batFile, cmdFile] = ['flun_el.vbs', 'flun_el_output.txt', 'flun_el.bat', 'flun_el.cmd']
|
|
57
|
+
.map(name => path.join(tmpDir, name)),
|
|
58
|
+
escapedCmd = cmd.replace(/"/g, '""').replace(/%/g, "%%"),
|
|
59
|
+
vbs = readFileSafe(vbsPath).replace(/{command}/g, escapedCmd);
|
|
60
|
+
|
|
61
|
+
// 写入VBS文件
|
|
62
|
+
try {
|
|
63
|
+
fs.writeFileSync(vbsFile, vbs, 'utf8');
|
|
64
|
+
} catch (writeError) {
|
|
65
|
+
return p.callback(writeError, '', '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
exec(`wscript.exe "${vbsFile}"`, { timeout: 20000 }, error => {
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
const output = readFileSafe(outFile).replace(/\r\n$|\n$/, '').trim();
|
|
71
|
+
|
|
72
|
+
// 清理临时文件
|
|
73
|
+
[batFile, cmdFile, outFile, vbsFile].forEach(f => {
|
|
74
|
+
try { fs.unlinkSync(f) } catch (e) { }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (error) return p.callback(error, '', '');
|
|
78
|
+
p.callback(null, output, '');
|
|
79
|
+
}, 1600);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 使用 sudo 方式提升权限(与 elevate 相似但体验更好)
|
|
85
|
+
* >查看定义:@see {@link sudo}
|
|
86
|
+
* @param {string} cmd - 要使用提升权限执行的命令
|
|
87
|
+
* @param {Object} [options] - 传递给 child_process.exec 的选项
|
|
88
|
+
* @param {string} [options.cwd] - 工作目录
|
|
89
|
+
* @param {number} [options.timeout] - 超时时间(毫秒)
|
|
90
|
+
* @param {(error: Error|null, stdout: string, stderr: string) => void} [callback] - 执行完成后的回调函数
|
|
91
|
+
* @returns {void}
|
|
92
|
+
* @example
|
|
93
|
+
* import { sudo } from '@flun/windows';
|
|
94
|
+
*
|
|
95
|
+
* // 基本用法 - 命令、选项和回调
|
|
96
|
+
* sudo('echo "Hello World" && whoami', {}, (error, stdout, stderr) => {
|
|
97
|
+
* if (error) {
|
|
98
|
+
* console.error('执行失败:', error);
|
|
99
|
+
* } else {
|
|
100
|
+
* console.log('执行成功,输出:', stdout);
|
|
101
|
+
* console.log('错误输出:', stderr);
|
|
102
|
+
* }
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // 带选项参数
|
|
106
|
+
* sudo('echo "Hello World" && whoami', {cwd:'C:\\'}, callback);
|
|
107
|
+
*
|
|
108
|
+
* sudo('echo "Hello World" && whoami' ); // 无回调
|
|
109
|
+
*/
|
|
110
|
+
const sudo = (cmd, options, callback) => {
|
|
111
|
+
const p = _params(options, callback);
|
|
112
|
+
exec(`sudo ${cmd}`, p.options, p.callback);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 检查当前用户是否拥有管理员权限
|
|
117
|
+
* >查看定义:@see {@link isAdminUser}
|
|
118
|
+
* @param {(isAdmin: boolean) => void} callback - 回调函数,接收布尔值表示是否为管理员
|
|
119
|
+
* @returns {void}
|
|
120
|
+
* @example
|
|
121
|
+
* import { isAdminUser } from '@flun/windows';
|
|
122
|
+
* isAdminUser(isAdmin => {
|
|
123
|
+
* if (isAdmin) console.log('当前用户是管理员');
|
|
124
|
+
* else console.log('当前用户不是管理员');
|
|
125
|
+
* });
|
|
126
|
+
*/
|
|
127
|
+
const isAdminUser = callback => {
|
|
128
|
+
exec('NET SESSION', (err, so, se) => {
|
|
129
|
+
if (se.length !== 0) elevate('NET SESSION', (_err, _so, _se) => callback(_se.length === 0));
|
|
130
|
+
else callback(true);
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
134
|
export { elevate, sudo, isAdminUser, platform };
|
package/lib/daemon.js
CHANGED
|
@@ -12,22 +12,18 @@ const __dirname = getDirname(import.meta.url), writeFileAsync = promisify(fs.wri
|
|
|
12
12
|
* >查看定义:@see {@link Service}、{@link install}、{@link uninstall}、{@link start}、{@link stop}、
|
|
13
13
|
* {@link restart}、{@link exists}
|
|
14
14
|
* @example
|
|
15
|
-
* // 配置示例
|
|
16
15
|
* import { Service } from '@flun/windows';
|
|
16
|
+
* import config from './win-service.config.js';
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
22
|
-
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
18
|
+
* const svc = new Service(config);
|
|
19
|
+
* // 安装并自动启动服务(默认)
|
|
20
|
+
* svc.install();
|
|
23
21
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* // svc.stop(); // 停止服务
|
|
30
|
-
* // svc.uninstall(); // 卸载服务
|
|
22
|
+
* // 只安装,不自动启动
|
|
23
|
+
* // svc.install(undefined, false);
|
|
24
|
+
*
|
|
25
|
+
* // 其余方法
|
|
26
|
+
* // svc.start(); svc.stop(); svc.uninstall();
|
|
31
27
|
*/
|
|
32
28
|
class Service extends EventEmitter {
|
|
33
29
|
/**
|
|
@@ -88,7 +84,7 @@ class Service extends EventEmitter {
|
|
|
88
84
|
scriptOptions = '', stopparentfirst = false, abortOnError = false, grow = 0.25, logpath = null, logmode = 'rotate',
|
|
89
85
|
description = '', execPath = process.execPath, workingDirectory = process.cwd(),
|
|
90
86
|
logOnAs = {},// 嵌套配置对象
|
|
91
|
-
env, logging, allowServiceLogon, // 其它配置
|
|
87
|
+
env, logging, allowServiceLogon, quiet = false, // 其它配置
|
|
92
88
|
} = config, domain = process.env.COMPUTERNAME;
|
|
93
89
|
|
|
94
90
|
// 私有字段
|
|
@@ -120,6 +116,7 @@ class Service extends EventEmitter {
|
|
|
120
116
|
|
|
121
117
|
/** 环境变量配置 */
|
|
122
118
|
this.env = env, this.logging = logging, this.allowServiceLogon = allowServiceLogon;
|
|
119
|
+
this.quiet = Boolean(quiet); // 为 true 时不打印执行结果,只发事件;嵌入生产环境时建议开启
|
|
123
120
|
}
|
|
124
121
|
|
|
125
122
|
// 进程名称
|
|
@@ -220,10 +217,30 @@ class Service extends EventEmitter {
|
|
|
220
217
|
}
|
|
221
218
|
}
|
|
222
219
|
|
|
220
|
+
/**
|
|
221
|
+
* 生成 winsw 配置 XML(不写文件,不安装)
|
|
222
|
+
* >查看定义:@see {@link toXml}
|
|
223
|
+
* @returns {string} XML 字符串
|
|
224
|
+
*/
|
|
225
|
+
toXml() { return this.#xml }
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 生成 winsw 可执行文件(不安装)
|
|
229
|
+
* >查看定义:@see {@link toExe}
|
|
230
|
+
* @param {string} [dir] - 目标目录(默认与 install 相同,取脚本所在目录下的 daemon 子目录)
|
|
231
|
+
* @returns {Promise<void>}
|
|
232
|
+
*/
|
|
233
|
+
async toExe(dir) {
|
|
234
|
+
const targetDir = this.directory(dir);
|
|
235
|
+
if (!fs.existsSync(targetDir)) await mkdirAsync(targetDir, { recursive: true });
|
|
236
|
+
await new Promise((resolve, reject) => createExe(this.id, targetDir, error => error ? reject(error) : resolve()));
|
|
237
|
+
}
|
|
238
|
+
|
|
223
239
|
/**
|
|
224
240
|
* 将脚本安装为Windows服务
|
|
225
241
|
* >查看定义:@see {@link install}
|
|
226
242
|
* @param {string} [dir] - 服务文件将保存的目录(默认为脚本所在目录)
|
|
243
|
+
* @param {boolean} [autoStart=true] - 安装成功后是否立即启动服务
|
|
227
244
|
* @returns {Promise<void>}
|
|
228
245
|
* @event install - 当安装过程完成时触发
|
|
229
246
|
* @event alreadyinstalled - 如果服务已安装时触发
|
|
@@ -231,25 +248,19 @@ class Service extends EventEmitter {
|
|
|
231
248
|
* @event error - 在错误发生时触发
|
|
232
249
|
* @example
|
|
233
250
|
* import { Service } from '@flun/windows';
|
|
234
|
-
*
|
|
235
|
-
* name: 'Hello World', // 服务名称
|
|
236
|
-
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
237
|
-
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
251
|
+
* import config from './win-service.config.js';
|
|
238
252
|
*
|
|
239
|
-
*
|
|
240
|
-
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
241
|
-
* });
|
|
253
|
+
* const svc = new Service(config);
|
|
242
254
|
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* svc.start();
|
|
246
|
-
* });
|
|
255
|
+
* // 安装并在完成后自动启动(默认)
|
|
256
|
+
* svc.install();
|
|
247
257
|
*
|
|
248
|
-
*
|
|
258
|
+
* // 只安装,不启动
|
|
259
|
+
* // svc.install(undefined, false);
|
|
249
260
|
*/
|
|
250
|
-
async install(dir) {
|
|
261
|
+
async install(dir, autoStart = true) {
|
|
251
262
|
// 检查是否已安装
|
|
252
|
-
if (this.exists === 3) return
|
|
263
|
+
if (this.exists === 3) return this.#report('安装跳过,服务已经存在;'), this.emit('alreadyinstalled');
|
|
253
264
|
|
|
254
265
|
const targetDir = this.directory(dir);
|
|
255
266
|
if (!fs.existsSync(targetDir)) await mkdirAsync(targetDir, { recursive: true });
|
|
@@ -261,7 +272,10 @@ class Service extends EventEmitter {
|
|
|
261
272
|
});
|
|
262
273
|
|
|
263
274
|
// 执行安装命令
|
|
264
|
-
await this.#execute(`"${this.#resPath(this.#exe)}" install`), await this.#sleep(2)
|
|
275
|
+
await this.#execute(`"${this.#resPath(this.#exe)}" install`), await this.#sleep(2);
|
|
276
|
+
if (autoStart) await this.start();
|
|
277
|
+
this.#report('服务安装完成;');
|
|
278
|
+
this.emit('install');
|
|
265
279
|
} catch (error) {
|
|
266
280
|
this.emit('error', error);
|
|
267
281
|
throw error;
|
|
@@ -274,31 +288,21 @@ class Service extends EventEmitter {
|
|
|
274
288
|
* @param {number} [waitTime=2] - 等待winsw.exe完成卸载命令的秒数
|
|
275
289
|
* @returns {Promise<void>}
|
|
276
290
|
* @event uninstall - 当卸载过程完成时触发
|
|
277
|
-
* @event alreadyuninstalled - 如果服务已卸载时触发
|
|
278
291
|
* @example
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
292
|
+
* import { Service } from '@flun/windows';
|
|
293
|
+
* import config from './win-service.config.js';
|
|
294
|
+
*
|
|
295
|
+
* const svc = new Service(config);
|
|
284
296
|
*
|
|
285
|
-
* // 传递给node进程的选项
|
|
286
|
-
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
287
|
-
* });
|
|
288
297
|
* svc.on('uninstall', ()=>{
|
|
289
298
|
* console.log('卸载完成');
|
|
290
299
|
* console.log('服务是否存在:', svc.exists);
|
|
291
|
-
* // svc.exists 返回值说明:
|
|
292
|
-
* // 0: 服务和相关文件没有
|
|
293
|
-
* // 1: 服务已注册但相关文件不存在(异常情况)
|
|
294
|
-
* // 2: 服务未注册但相关文件存在(文件残留)
|
|
295
|
-
* // 3: 服务已注册且相关文件存在(正常状态)
|
|
296
300
|
* });
|
|
297
301
|
*
|
|
298
302
|
* svc.uninstall();
|
|
299
303
|
*/
|
|
300
304
|
async uninstall(waitTime = 2) {
|
|
301
|
-
if (!this.exists) return
|
|
305
|
+
if (!this.exists) return this.#report('卸载已跳过,服务已经不在;'), this.emit('alreadyuninstalled');
|
|
302
306
|
|
|
303
307
|
const uninstaller = async () => {
|
|
304
308
|
await this.#execute(`"${this.#resPath(this.#exe)}" uninstall`), await this.#sleep(waitTime);
|
|
@@ -308,6 +312,7 @@ class Service extends EventEmitter {
|
|
|
308
312
|
console.error(`删除目录失败: ${error.message}`);
|
|
309
313
|
}
|
|
310
314
|
|
|
315
|
+
this.#report('服务卸载完成;');
|
|
311
316
|
this.emit('uninstall');
|
|
312
317
|
};
|
|
313
318
|
|
|
@@ -321,14 +326,10 @@ class Service extends EventEmitter {
|
|
|
321
326
|
* @event start - 当服务启动时触发
|
|
322
327
|
* @example
|
|
323
328
|
* import { Service } from '@flun/windows';
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
329
|
+
* import config from './win-service.config.js';
|
|
330
|
+
*
|
|
331
|
+
* const svc = new Service(config);
|
|
328
332
|
*
|
|
329
|
-
* // 传递给node进程的选项
|
|
330
|
-
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
331
|
-
* });
|
|
332
333
|
* svc.on('start', ()=>{
|
|
333
334
|
* console.log('服务已启动');
|
|
334
335
|
* });
|
|
@@ -340,7 +341,7 @@ class Service extends EventEmitter {
|
|
|
340
341
|
|
|
341
342
|
try {
|
|
342
343
|
const { stderr } = await this.#execute(`NET START "${this.#serviceName}"`);
|
|
343
|
-
if (!stderr) return this.emit('start');
|
|
344
|
+
if (!stderr) return this.#report('服务已启动;'), this.emit('start');
|
|
344
345
|
throw new Error(stderr);
|
|
345
346
|
} catch (error) {
|
|
346
347
|
if (error.message.includes('already been started')) return this.log.warn('尝试启动服务失败,因为服务已经在运行;');
|
|
@@ -356,14 +357,9 @@ class Service extends EventEmitter {
|
|
|
356
357
|
* @event alreadystopped - 当服务已经停止时触发
|
|
357
358
|
* @example
|
|
358
359
|
* import { Service } from '@flun/windows';
|
|
359
|
-
*
|
|
360
|
-
* name: 'Hello World', // 服务名称
|
|
361
|
-
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
362
|
-
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
360
|
+
* import config from './win-service.config.js';
|
|
363
361
|
*
|
|
364
|
-
*
|
|
365
|
-
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
366
|
-
* });
|
|
362
|
+
* const svc = new Service(config);
|
|
367
363
|
* svc.on('stop', ()=>{
|
|
368
364
|
* console.log('服务已停止');
|
|
369
365
|
* });
|
|
@@ -372,7 +368,7 @@ class Service extends EventEmitter {
|
|
|
372
368
|
*/
|
|
373
369
|
async stop() {
|
|
374
370
|
try {
|
|
375
|
-
await this.#execute(`NET STOP "${this.#serviceName}"`), this.emit('stop');
|
|
371
|
+
await this.#execute(`NET STOP "${this.#serviceName}"`), this.#report('服务已停止;'), this.emit('stop');
|
|
376
372
|
} catch (error) {
|
|
377
373
|
if (error.code === 2) return this.log.warn('服务未运行或已停止;'), this.emit('alreadystopped');
|
|
378
374
|
this.emit('error', error);
|
|
@@ -385,10 +381,9 @@ class Service extends EventEmitter {
|
|
|
385
381
|
* @returns {Promise<void>}
|
|
386
382
|
* @example
|
|
387
383
|
* import { Service } from '@flun/windows';
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* });
|
|
384
|
+
* import config from './win-service.config.js';
|
|
385
|
+
*
|
|
386
|
+
* const svc = new Service(config);
|
|
392
387
|
*
|
|
393
388
|
* svc.restart();
|
|
394
389
|
*/
|
|
@@ -415,7 +410,7 @@ class Service extends EventEmitter {
|
|
|
415
410
|
executor(cmd, { ...options, shell: true, windowsHide: true }, (error, stdout, stderr) => {
|
|
416
411
|
if (error) {
|
|
417
412
|
if (isPermissionError(error.message)) reject(new Error('权限被拒绝,请以管理员身份重新运行此脚本;'));
|
|
418
|
-
else console.error(error.toString())
|
|
413
|
+
else { if (!this.quiet) console.error(error.toString()); reject(error); }
|
|
419
414
|
}
|
|
420
415
|
else resolve({ stdout, stderr });
|
|
421
416
|
});
|
|
@@ -427,6 +422,12 @@ class Service extends EventEmitter {
|
|
|
427
422
|
* @param {number} seconds - 睡眠秒数
|
|
428
423
|
* @returns {Promise<void>}
|
|
429
424
|
*/
|
|
425
|
+
/**
|
|
426
|
+
* 按 quiet 配置决定是否打印执行结果;
|
|
427
|
+
* @param {string} msg 结果消息
|
|
428
|
+
*/
|
|
429
|
+
#report = msg => { if (!this.quiet) console.log(msg) };
|
|
430
|
+
|
|
430
431
|
#sleep = seconds => new Promise(r => setTimeout(r, seconds * 1000));
|
|
431
432
|
}
|
|
432
433
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flun/windows",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "支持 Windows 服务、事件日志、UAC 以及多种与操作系统交互的辅助方法",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"windows",
|
|
@@ -23,12 +23,15 @@
|
|
|
23
23
|
".": "./index.js"
|
|
24
24
|
},
|
|
25
25
|
"main": "index.js",
|
|
26
|
+
"bin": {
|
|
27
|
+
"windows": "bin/cli.js"
|
|
28
|
+
},
|
|
26
29
|
"files": [
|
|
27
30
|
"lib",
|
|
28
31
|
"bin",
|
|
29
32
|
"index.d.ts",
|
|
30
33
|
"index.js",
|
|
31
|
-
"
|
|
34
|
+
"win-service.config.js",
|
|
32
35
|
"copy-files.js",
|
|
33
36
|
"CHANGELOG.md",
|
|
34
37
|
"README.md",
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
],
|
|
37
40
|
"preferGlobal": true,
|
|
38
41
|
"dependencies": {
|
|
42
|
+
"chalk": "^6.0.1",
|
|
39
43
|
"xml": "1.0.1",
|
|
40
44
|
"yargs": "^18.2.0"
|
|
41
45
|
},
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows 服务配置模板
|
|
3
|
+
* 带 * 的为必填项,其余可删除或按需保留;
|
|
4
|
+
* 本配置既支持命令行方式(如 windows install),
|
|
5
|
+
* 也支持在函数方式中导入:import config from './win-service.config.js';
|
|
6
|
+
* 具体用法见 README 的「命令行」与「函数方式」章节;
|
|
7
|
+
*
|
|
8
|
+
* 关于 quiet:默认 false,各方法执行后会在终端打印结果(如"服务安装完成;");
|
|
9
|
+
* 若本包嵌入到自己的生产项目,终端输出会混入宿主日志,此时设为 true,
|
|
10
|
+
* 库只发事件不打印,由宿主自行监听 install/start/stop/uninstall/error 等事件;
|
|
11
|
+
* 命令行方式恒为 quiet,不受此字段影响;
|
|
12
|
+
*/
|
|
13
|
+
export default {
|
|
14
|
+
// * 服务名称,用于服务管理与注册表标识;不能含 <>:"\/|?*
|
|
15
|
+
name: 'MyApp',
|
|
16
|
+
// * 启动服务的入口脚本绝对路径
|
|
17
|
+
script: 'C:\\path\\to\\main.js',
|
|
18
|
+
|
|
19
|
+
// 服务描述,显示在服务管理器中
|
|
20
|
+
description: 'Node.js 服务',
|
|
21
|
+
// 传递给 node 进程的选项,可为数组或空格分隔的字符串
|
|
22
|
+
nodeOptions: ['--harmony'],
|
|
23
|
+
// 传递给脚本的选项
|
|
24
|
+
scriptOptions: '',
|
|
25
|
+
// node 可执行文件路径,默认取当前进程的 execPath
|
|
26
|
+
execPath: process.execPath,
|
|
27
|
+
// 服务工作目录,默认取当前工作目录
|
|
28
|
+
workingDirectory: process.cwd(),
|
|
29
|
+
|
|
30
|
+
// 无响应/故障前的最大重试次数,默认 null(不启用)
|
|
31
|
+
maxRetries: null,
|
|
32
|
+
// 60 秒内最大重启次数,0 表示不启用,默认 3
|
|
33
|
+
maxRestarts: 3,
|
|
34
|
+
// 停止服务的超时秒数,默认 30
|
|
35
|
+
stoptimeout: 30,
|
|
36
|
+
// 脚本停止后等待重启的秒数,默认 1
|
|
37
|
+
wait: 1,
|
|
38
|
+
// 重启等待时间的增长百分比,默认 0.25
|
|
39
|
+
grow: 0.25,
|
|
40
|
+
// 脚本运行错误时是否退出进程,默认 false
|
|
41
|
+
abortOnError: false,
|
|
42
|
+
// 是否先停止父进程,默认 false
|
|
43
|
+
stopparentfirst: false,
|
|
44
|
+
|
|
45
|
+
// 日志文件路径,默认与可执行文件同目录
|
|
46
|
+
logpath: null,
|
|
47
|
+
// 日志模式:rotate(默认)、truncate、append
|
|
48
|
+
logmode: 'rotate',
|
|
49
|
+
|
|
50
|
+
// 环境变量,形式为 { name, value } 或同构数组
|
|
51
|
+
env: { name: 'NODE_ENV', value: 'production' },
|
|
52
|
+
|
|
53
|
+
// 日志配置对象,可替代 logmode;含 mode、pattern、sizeThreshold、keepFiles
|
|
54
|
+
logging: undefined,
|
|
55
|
+
|
|
56
|
+
// 服务登录凭据;默认使用 LocalSystem
|
|
57
|
+
logOnAs: {
|
|
58
|
+
account: undefined,
|
|
59
|
+
password: null,
|
|
60
|
+
domain: process.env.COMPUTERNAME,
|
|
61
|
+
mungeCredentialsAfterInstall: true,
|
|
62
|
+
},
|
|
63
|
+
// 是否允许服务登录,默认 undefined
|
|
64
|
+
allowServiceLogon: undefined,
|
|
65
|
+
|
|
66
|
+
// 为 true 时不打印执行结果,只发事件;嵌入生产项目时建议开启
|
|
67
|
+
quiet: false,
|
|
68
|
+
};
|
package/sevWin.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { Service } from '@flun/windows'; // 可选参数(EventLogger,elevate, sudo, isAdminUser, kill, list)
|
|
2
|
-
|
|
3
|
-
const serviceName = 'TestApp', scriptPath = 'D:\\test\\dev.js', // 请根据实际路径修改
|
|
4
|
-
svc = new Service({
|
|
5
|
-
name: serviceName,
|
|
6
|
-
description: 'Node.js 开发服务器',
|
|
7
|
-
script: scriptPath,
|
|
8
|
-
nodeOptions: ['--harmony', '--max-old-space-size=4096'],
|
|
9
|
-
env: { name: "NODE_ENV", value: "production" },
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const installService = () => {
|
|
13
|
-
console.log('🚀 开始安装服务...');
|
|
14
|
-
svc.on('install', () => svc.start());
|
|
15
|
-
svc.on('start', () => console.log('✅ 安装成功,服务已启动!!'));
|
|
16
|
-
svc.install();
|
|
17
|
-
}
|
|
18
|
-
installService();
|