@flun/windows 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +360 -360
- package/index.js +8 -8
- package/lib/cmd.js +53 -53
- package/lib/daemon.js +421 -421
- package/lib/eventlog.js +225 -225
- package/package.json +58 -58
package/lib/daemon.js
CHANGED
|
@@ -1,422 +1,422 @@
|
|
|
1
|
-
import { exec, execSync, promisify, path, fs, EventEmitter, isPermissionError, getDirname } from './shared.js';
|
|
2
|
-
import { elevate, sudo as binSudo } from './binaries.js';
|
|
3
|
-
import { generateXml, createExe } from './winsw.js';
|
|
4
|
-
import { EventLogger } from './eventlog.js';
|
|
5
|
-
|
|
6
|
-
const __dirname = getDirname(import.meta.url), writeFileAsync = promisify(fs.writeFile), mkdirAsync = promisify(fs.mkdir),
|
|
7
|
-
daemonDir = 'daemon', wrapper = path.resolve(path.join(__dirname, './wrapper.js')), nameRegex = /[<>:"\\/|?*]/g;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Windows服务管理类,提供创建、安装、卸载、启动、停止、重启和查询服务状态等功能
|
|
11
|
-
* - 用于将Node.js脚本作为Windows服务运行,支持自动重启、日志记录和权限提升等功能
|
|
12
|
-
* >查看定义:@see {@link Service}、{@link install}、{@link uninstall}、{@link start}、{@link stop}、{@link restart}、{@link exists}
|
|
13
|
-
* @example
|
|
14
|
-
* // 配置示例
|
|
15
|
-
* import { Service } from '@flun/windows';
|
|
16
|
-
*
|
|
17
|
-
* // 创建服务对象
|
|
18
|
-
* const svc = new Service({
|
|
19
|
-
* name: 'Hello World', // 服务名称
|
|
20
|
-
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
21
|
-
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
22
|
-
*
|
|
23
|
-
* // 传递给node进程的选项
|
|
24
|
-
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
25
|
-
* });
|
|
26
|
-
* svc.install(); // 安装服务
|
|
27
|
-
* // svc.start(); // 启动服务
|
|
28
|
-
* // svc.stop(); // 停止服务
|
|
29
|
-
* // svc.uninstall(); // 卸载服务
|
|
30
|
-
*/
|
|
31
|
-
class Service extends EventEmitter {
|
|
32
|
-
/**
|
|
33
|
-
* 创建服务实例
|
|
34
|
-
* @param {Object} config - 服务配置
|
|
35
|
-
* @param {string} config.name - 服务名称
|
|
36
|
-
* @param {string} config.script - 启动服务的入口脚本路径
|
|
37
|
-
* @param {number} [config.maxRetries=null] - 服务无响应/故障之前的最大重试次数(默认忽略)
|
|
38
|
-
* @param {number} [config.maxRestarts=3] - 在60秒内最大重启次数(0表示不启用),超过则停止进程
|
|
39
|
-
* @param {number} [config.stoptimeout=30] - 停止服务的超时秒数
|
|
40
|
-
* @param {number} [config.wait=1] - 脚本停止后等待重新启动的秒数
|
|
41
|
-
* @param {string} [config.nodeOptions='--harmony'] - 传递给node进程的选项
|
|
42
|
-
* @param {string} [config.scriptOptions=''] - 传递给脚本的选项
|
|
43
|
-
* @param {boolean} [config.stopparentfirst=false] - 是否先停止父进程
|
|
44
|
-
* @param {boolean} [config.abortOnError=false] - 脚本运行错误时是否退出进程
|
|
45
|
-
* @param {number} [config.grow=0.25] - 重启等待时间的增长百分比
|
|
46
|
-
* @param {string|null} [config.logpath=null] - 日志文件路径(默认与可执行文件相同目录)
|
|
47
|
-
* @param {string} [config.logmode='rotate'] - 日志模式(rotate, truncate, append)
|
|
48
|
-
* @param {string} [config.description=''] - 服务描述
|
|
49
|
-
* @param {string} [config.execPath=process.execPath] - 可执行文件路径
|
|
50
|
-
* @param {string} [config.workingDirectory=process.cwd()] - 服务进程工作目录
|
|
51
|
-
* @param {Object} [config.logOnAs] - 服务登录凭据配置
|
|
52
|
-
* @param {string} [config.logOnAs.account] - 登录账户
|
|
53
|
-
* @param {string|null} [config.logOnAs.password] - 登录密码
|
|
54
|
-
* @param {string} [config.logOnAs.domain] - 域名(默认使用COMPUTERNAME)
|
|
55
|
-
* @param {boolean} [config.logOnAs.mungeCredentialsAfterInstall=true] - 安装后是否混淆凭据
|
|
56
|
-
* @param {Object} [config.sudo] - 提升权限配置
|
|
57
|
-
* @param {boolean|null} [config.sudo.enabled] - 是否启用sudo方式提升权限
|
|
58
|
-
* @param {Object} [config.env] - 环境变量配置
|
|
59
|
-
* @param {Object} [config.logging] - 日志配置
|
|
60
|
-
* @param {boolean} [config.allowServiceLogon] - 是否允许服务登录
|
|
61
|
-
*/
|
|
62
|
-
constructor(config) {
|
|
63
|
-
super(), this.#validateConfig(config), this.#initializeProperties(config);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
*验证配置
|
|
68
|
-
* @param {Object} config - 服务配置对象
|
|
69
|
-
*/
|
|
70
|
-
#validateConfig(config) {
|
|
71
|
-
if (!config.name || !config.script) throw new Error('服务名称和脚本路径不可为空;')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 私有字段
|
|
75
|
-
#name = null;
|
|
76
|
-
#eventlog;
|
|
77
|
-
#directory;
|
|
78
|
-
|
|
79
|
-
// 名称过滤方法
|
|
80
|
-
#filterName(name) {
|
|
81
|
-
return name ? name.replace(nameRegex, '') : console.log('服务名称无效');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 初始化属性
|
|
85
|
-
#initializeProperties(config) {
|
|
86
|
-
const {
|
|
87
|
-
// 基础配置
|
|
88
|
-
name, script, maxRetries = null, maxRestarts = 3, stoptimeout = 30, wait = 1, nodeOptions = '--harmony',
|
|
89
|
-
scriptOptions = '', stopparentfirst = false, abortOnError = false, grow = 0.25, logpath = null, logmode = 'rotate',
|
|
90
|
-
description = '', execPath = process.execPath, workingDirectory = process.cwd(),
|
|
91
|
-
logOnAs = {}, sudo = {}, // 嵌套配置对象
|
|
92
|
-
env, logging, allowServiceLogon, // 其它配置
|
|
93
|
-
} = config, domain = process.env.COMPUTERNAME;
|
|
94
|
-
|
|
95
|
-
// 私有字段
|
|
96
|
-
this.#name = this.#filterName(name), this.#eventlog = null, this.#directory = script ? path.dirname(script) : null;
|
|
97
|
-
|
|
98
|
-
// 公共属性
|
|
99
|
-
this.maxRetries = maxRetries; // 服务无响应/故障之前的最大重试次数(默认忽略);
|
|
100
|
-
this.maxRestarts = maxRestarts; // 在60秒内最大重启次数(0表示不启用),超过则停止进程;
|
|
101
|
-
this.stoptimeout = stoptimeout; // 停止服务的超时时间(默认:30秒);
|
|
102
|
-
this.wait = Number(wait); // 脚本停止后等待重新启动的秒数(默认:1秒);
|
|
103
|
-
this.nodeOptions = nodeOptions; // 传递给node进程的选项;
|
|
104
|
-
this.scriptOptions = scriptOptions; // 传递给脚本的选项;
|
|
105
|
-
|
|
106
|
-
this.stopparentfirst = stopparentfirst; // 是否先停止父进程(默认:false);
|
|
107
|
-
this.abortOnError = Boolean(abortOnError); // 当遇到导致node.js脚本无法运行错误时是否退出进程(默认:false);
|
|
108
|
-
this.grow = Number(grow); // 重启等待时间的增长百分比(默认:0.25);
|
|
109
|
-
this.logpath = logpath; // 日志文件路径(默认:与可执行文件相同的目录);
|
|
110
|
-
this.logmode = logmode; // 日志模式(默认:rotate);可选值: rotate, truncate, append;
|
|
111
|
-
this.description = description; // 服务描述;
|
|
112
|
-
this.script = path.resolve(script); // 服务启动脚本的绝对路径;
|
|
113
|
-
this.execPath = path.resolve(execPath); // 启动脚本可执行文件的绝对路径;
|
|
114
|
-
this.workingdirectory = workingDirectory; // 服务进程启动工作目录的完整路径(默认:当前工作目录);
|
|
115
|
-
|
|
116
|
-
/** 服务登录凭据配置 */
|
|
117
|
-
this.logOnAs = {
|
|
118
|
-
account: undefined, password: null, domain, mungeCredentialsAfterInstall: true, ...logOnAs
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// 提升权限配置
|
|
122
|
-
this.sudo = { enabled: null, ...sudo };
|
|
123
|
-
|
|
124
|
-
/** 环境变量配置 */
|
|
125
|
-
this.env = env, this.logging = logging, this.allowServiceLogon = allowServiceLogon;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 进程名称
|
|
129
|
-
get name() {
|
|
130
|
-
return this.#name;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
set name(value) {
|
|
134
|
-
this.#name = this.#filterName(value);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// 进程ID
|
|
138
|
-
get id() {
|
|
139
|
-
return this.name;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 可执行文件名
|
|
143
|
-
get #exe() {
|
|
144
|
-
return `${this.id}.exe`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 服务名称(用于Windows服务管理)
|
|
148
|
-
get #serviceName() {
|
|
149
|
-
return this.id;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// 生成服务的XML配置
|
|
153
|
-
get #xml() {
|
|
154
|
-
// 提取需要的属性
|
|
155
|
-
const { script, scriptOptions, name, grow, wait, maxRestarts, abortOnError, stopparentfirst, maxRetries, id, nodeOptions,
|
|
156
|
-
description, logpath, execPath, logOnAs, workingdirectory, stoptimeout, logmode, env, logging, allowServiceLogon } = this,
|
|
157
|
-
wrapperArgs = ['--file', script, `--scriptoptions=${scriptOptions}`, '--log', `${name} 包装器`, '--grow', grow, '--wait', wait,
|
|
158
|
-
'--maxrestarts', maxRestarts, '--abortonerror', abortOnError ? 'y' : 'n', '--stopparentfirst', stopparentfirst,
|
|
159
|
-
...(maxRetries !== null ? ['--maxretries', maxRetries] : [])];
|
|
160
|
-
|
|
161
|
-
return generateXml({
|
|
162
|
-
name, id, nodeOptions, script: wrapper, scriptOptions, wrapperArgs, description, logpath, execPath, logOnAs, workingdirectory,
|
|
163
|
-
stopparentfirst, stoptimeout, logmode, env, logging, allowServiceLogon
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* 解析脚本保存的目录
|
|
169
|
-
* @param {string} [dir] - 自定义目录路径,若不提供则使用脚本所在目录
|
|
170
|
-
* @returns {string} 服务文件的完整保存目录
|
|
171
|
-
*/
|
|
172
|
-
directory(dir) {
|
|
173
|
-
if (dir) this.#directory = path.resolve(dir);
|
|
174
|
-
return path.resolve(path.join(this.#directory, daemonDir));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
#resPath(...paths) {
|
|
178
|
-
return path.resolve(this.root, ...paths);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* 进程文件存储的根目录
|
|
183
|
-
* @returns {string}
|
|
184
|
-
*/
|
|
185
|
-
get root() {
|
|
186
|
-
return this.directory();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* 服务事件日志记录器实例
|
|
191
|
-
* @returns {EventLogger}
|
|
192
|
-
*/
|
|
193
|
-
get log() {
|
|
194
|
-
if (this.#eventlog !== null) return this.#eventlog;
|
|
195
|
-
this.#eventlog = new EventLogger(`${this.name} 监视器`);
|
|
196
|
-
return this.#eventlog;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* 服务是否存在及其状态
|
|
201
|
-
* - 0: 不存在
|
|
202
|
-
* - 1: 存在服务但无文件
|
|
203
|
-
* - 2: 存在文件但无服务
|
|
204
|
-
* - 3: 完整存在
|
|
205
|
-
* >查看定义:@see {@link exists}
|
|
206
|
-
* @returns {number} 状态码 0-3
|
|
207
|
-
*/
|
|
208
|
-
get exists() {
|
|
209
|
-
const hasFiles = fs.existsSync(this.#resPath(`${this.id}.exe`)) && fs.existsSync(this.#resPath(`${this.id}.xml`));
|
|
210
|
-
try {
|
|
211
|
-
execSync(`sc query "${this.#serviceName}"`, { stdio: 'ignore' });
|
|
212
|
-
return hasFiles ? 3 : 1;
|
|
213
|
-
} catch {
|
|
214
|
-
return hasFiles ? 2 : 0;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* 将脚本安装为Windows服务
|
|
220
|
-
* >查看定义:@see {@link install}
|
|
221
|
-
* @param {string} [dir] - 服务文件将保存的目录(默认为脚本所在目录)
|
|
222
|
-
* @returns {Promise<void>}
|
|
223
|
-
* @event install - 当安装过程完成时触发
|
|
224
|
-
* @event alreadyinstalled - 如果服务已安装时触发
|
|
225
|
-
* @event invalidinstallation - 如果检测到安装但缺少必需文件时触发
|
|
226
|
-
* @event error - 在错误发生时触发
|
|
227
|
-
* @example
|
|
228
|
-
* import { Service } from '@flun/windows';
|
|
229
|
-
* const svc = new Service({
|
|
230
|
-
* name: 'Hello World', // 服务名称
|
|
231
|
-
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
232
|
-
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
233
|
-
*
|
|
234
|
-
* // 传递给node进程的选项
|
|
235
|
-
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
236
|
-
* });
|
|
237
|
-
*
|
|
238
|
-
* // 监听安装完成事件
|
|
239
|
-
* svc.on('install', ()=>{
|
|
240
|
-
* svc.start();
|
|
241
|
-
* });
|
|
242
|
-
*
|
|
243
|
-
* svc.install();
|
|
244
|
-
*/
|
|
245
|
-
async install(dir) {
|
|
246
|
-
// 检查是否已安装
|
|
247
|
-
if (this.exists === 3) return console.log('安装跳过,服务已经存在;'), this.emit('alreadyinstalled');
|
|
248
|
-
|
|
249
|
-
const targetDir = this.directory(dir);
|
|
250
|
-
if (!fs.existsSync(targetDir)) await mkdirAsync(targetDir, { recursive: true });
|
|
251
|
-
try {
|
|
252
|
-
// 写入配置文件&创建可执行文件
|
|
253
|
-
await writeFileAsync(this.#resPath(`${this.id}.xml`), this.#xml);
|
|
254
|
-
await new Promise((resolve, reject) => {
|
|
255
|
-
createExe(this.id, targetDir, error => error ? reject(error) : resolve());
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// 执行安装命令
|
|
259
|
-
await this.#execute(`"${this.#resPath(this.#exe)}" install`), await this.#sleep(2), this.emit('install');
|
|
260
|
-
} catch (error) {
|
|
261
|
-
this.emit('error', error);
|
|
262
|
-
throw error;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* 卸载服务
|
|
268
|
-
* >查看定义:@see {@link uninstall}
|
|
269
|
-
* @param {number} [waitTime=2] - 等待winsw.exe完成卸载命令的秒数
|
|
270
|
-
* @returns {Promise<void>}
|
|
271
|
-
* @event uninstall - 当卸载过程完成时触发
|
|
272
|
-
* @event alreadyuninstalled - 如果服务已卸载时触发
|
|
273
|
-
* @example
|
|
274
|
-
* import { Service } from '@flun/windows';
|
|
275
|
-
* const svc = new Service({
|
|
276
|
-
* name: 'Hello World', // 服务名称
|
|
277
|
-
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
278
|
-
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
279
|
-
*
|
|
280
|
-
* // 传递给node进程的选项
|
|
281
|
-
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
282
|
-
* });
|
|
283
|
-
* svc.on('uninstall', ()=>{
|
|
284
|
-
* console.log('卸载完成');
|
|
285
|
-
* console.log('服务是否存在:', svc.exists);
|
|
286
|
-
* // svc.exists 返回值说明:
|
|
287
|
-
* // 0: 服务和相关文件没有
|
|
288
|
-
* // 1: 服务已注册但相关文件不存在(异常情况)
|
|
289
|
-
* // 2: 服务未注册但相关文件存在(文件残留)
|
|
290
|
-
* // 3: 服务已注册且相关文件存在(正常状态)
|
|
291
|
-
* });
|
|
292
|
-
*
|
|
293
|
-
* svc.uninstall();
|
|
294
|
-
*/
|
|
295
|
-
async uninstall(waitTime = 2) {
|
|
296
|
-
if (!this.exists) return console.log('卸载已跳过,服务已经不在;'), this.emit('alreadyuninstalled');
|
|
297
|
-
|
|
298
|
-
const uninstaller = async () => {
|
|
299
|
-
await this.#execute(`"${this.#resPath(this.#exe)}" uninstall`), await this.#sleep(waitTime);
|
|
300
|
-
try {
|
|
301
|
-
await fs.promises.rm(this.root, { recursive: true, force: true }), await this.#sleep(1);
|
|
302
|
-
} catch (error) {
|
|
303
|
-
console.error(`删除目录失败: ${error.message}`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
this.emit('uninstall');
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
this.once('stop', uninstaller), this.once('alreadystopped', uninstaller), await this.stop();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* 启动现有服务
|
|
314
|
-
* >查看定义:@see {@link start}
|
|
315
|
-
* @returns {Promise<void>}
|
|
316
|
-
* @event start - 当服务启动时触发
|
|
317
|
-
* @example
|
|
318
|
-
* import { Service } from '@flun/windows';
|
|
319
|
-
* const svc = new Service({
|
|
320
|
-
* name: 'Hello World', // 服务名称
|
|
321
|
-
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
322
|
-
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
323
|
-
*
|
|
324
|
-
* // 传递给node进程的选项
|
|
325
|
-
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
326
|
-
* });
|
|
327
|
-
* svc.on('start', ()=>{
|
|
328
|
-
* console.log('服务已启动');
|
|
329
|
-
* });
|
|
330
|
-
*
|
|
331
|
-
* svc.start();
|
|
332
|
-
*/
|
|
333
|
-
async start() {
|
|
334
|
-
if (this.exists !== 3) throw new Error(`启动 ${this.name} 服务条件缺失(exists返回码:${this.exists});`);
|
|
335
|
-
|
|
336
|
-
try {
|
|
337
|
-
const { stderr } = await this.#execute(`NET START "${this.#serviceName}"`);
|
|
338
|
-
if (!stderr) return this.emit('start'); // 如果没有错误,触发成功事件
|
|
339
|
-
throw new Error(stderr);
|
|
340
|
-
} catch (error) {
|
|
341
|
-
if (error.message.includes('already been started')) return this.log.warn('尝试启动服务失败,因为服务已经在运行;');
|
|
342
|
-
this.emit('error', error);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* 停止服务
|
|
348
|
-
* >查看定义:@see {@link stop}
|
|
349
|
-
* @returns {Promise<void>}
|
|
350
|
-
* @event stop - 当服务停止时触发
|
|
351
|
-
* @event alreadystopped - 当服务已经停止时触发
|
|
352
|
-
* @example
|
|
353
|
-
* import { Service } from '@flun/windows';
|
|
354
|
-
* const svc = new Service({
|
|
355
|
-
* name: 'Hello World', // 服务名称
|
|
356
|
-
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
357
|
-
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
358
|
-
*
|
|
359
|
-
* // 传递给node进程的选项
|
|
360
|
-
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
361
|
-
* });
|
|
362
|
-
* svc.on('stop', ()=>{
|
|
363
|
-
* console.log('服务已停止');
|
|
364
|
-
* });
|
|
365
|
-
*
|
|
366
|
-
* svc.stop();
|
|
367
|
-
*/
|
|
368
|
-
async stop() {
|
|
369
|
-
try {
|
|
370
|
-
await this.#execute(`NET STOP "${this.#serviceName}"`), this.emit('stop');
|
|
371
|
-
} catch (error) {
|
|
372
|
-
if (error.code === 2) return this.log.warn('服务未运行或已停止;'), this.emit('alreadystopped');
|
|
373
|
-
this.emit('error', error);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* 重启现有服务
|
|
379
|
-
* >查看定义:@see {@link restart}
|
|
380
|
-
* @returns {Promise<void>}
|
|
381
|
-
* @example
|
|
382
|
-
* import { Service } from '@flun/windows';
|
|
383
|
-
* const svc = new Service({
|
|
384
|
-
* name: 'Hello World',
|
|
385
|
-
* script: 'C:\\path\\to\\helloworld.js'
|
|
386
|
-
* });
|
|
387
|
-
*
|
|
388
|
-
* svc.restart();
|
|
389
|
-
*/
|
|
390
|
-
async restart() {
|
|
391
|
-
this.once('stop', () => this.start()), await this.stop();
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* 使用提升的权限执行命令
|
|
396
|
-
* @param {string} cmd - 要执行的命令
|
|
397
|
-
* @param {Object} [options={}] - 额外执行选项
|
|
398
|
-
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
399
|
-
*/
|
|
400
|
-
async #execute(cmd, options = {}) {
|
|
401
|
-
return new Promise((resolve, reject) => {
|
|
402
|
-
// 检查sudo是否为真
|
|
403
|
-
const executor = this.sudo.enabled ? (cmd, opts, cb) => binSudo(cmd, opts, cb) : elevate;
|
|
404
|
-
executor(cmd, { ...options, shell: true, windowsHide: true }, (error, stdout, stderr) => {
|
|
405
|
-
if (error) {
|
|
406
|
-
if (isPermissionError(error.message)) reject(new Error('权限被拒绝, 请以管理员身份重新运行此脚本;'));
|
|
407
|
-
else console.error(error.toString()), reject(error);
|
|
408
|
-
}
|
|
409
|
-
else resolve({ stdout, stderr });
|
|
410
|
-
});
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* 睡眠指定秒数
|
|
416
|
-
* @param {number} seconds - 睡眠秒数
|
|
417
|
-
* @returns {Promise<void>}
|
|
418
|
-
*/
|
|
419
|
-
#sleep = seconds => new Promise(r => setTimeout(r, seconds * 1000));
|
|
420
|
-
}
|
|
421
|
-
|
|
1
|
+
import { exec, execSync, promisify, path, fs, EventEmitter, isPermissionError, getDirname } from './shared.js';
|
|
2
|
+
import { elevate, sudo as binSudo } from './binaries.js';
|
|
3
|
+
import { generateXml, createExe } from './winsw.js';
|
|
4
|
+
import { EventLogger } from './eventlog.js';
|
|
5
|
+
|
|
6
|
+
const __dirname = getDirname(import.meta.url), writeFileAsync = promisify(fs.writeFile), mkdirAsync = promisify(fs.mkdir),
|
|
7
|
+
daemonDir = 'daemon', wrapper = path.resolve(path.join(__dirname, './wrapper.js')), nameRegex = /[<>:"\\/|?*]/g;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Windows服务管理类,提供创建、安装、卸载、启动、停止、重启和查询服务状态等功能
|
|
11
|
+
* - 用于将Node.js脚本作为Windows服务运行,支持自动重启、日志记录和权限提升等功能
|
|
12
|
+
* >查看定义:@see {@link Service}、{@link install}、{@link uninstall}、{@link start}、{@link stop}、{@link restart}、{@link exists}
|
|
13
|
+
* @example
|
|
14
|
+
* // 配置示例
|
|
15
|
+
* import { Service } from '@flun/windows';
|
|
16
|
+
*
|
|
17
|
+
* // 创建服务对象
|
|
18
|
+
* const svc = new Service({
|
|
19
|
+
* name: 'Hello World', // 服务名称
|
|
20
|
+
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
21
|
+
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
22
|
+
*
|
|
23
|
+
* // 传递给node进程的选项
|
|
24
|
+
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
25
|
+
* });
|
|
26
|
+
* svc.install(); // 安装服务
|
|
27
|
+
* // svc.start(); // 启动服务
|
|
28
|
+
* // svc.stop(); // 停止服务
|
|
29
|
+
* // svc.uninstall(); // 卸载服务
|
|
30
|
+
*/
|
|
31
|
+
class Service extends EventEmitter {
|
|
32
|
+
/**
|
|
33
|
+
* 创建服务实例
|
|
34
|
+
* @param {Object} config - 服务配置
|
|
35
|
+
* @param {string} config.name - 服务名称
|
|
36
|
+
* @param {string} config.script - 启动服务的入口脚本路径
|
|
37
|
+
* @param {number} [config.maxRetries=null] - 服务无响应/故障之前的最大重试次数(默认忽略)
|
|
38
|
+
* @param {number} [config.maxRestarts=3] - 在60秒内最大重启次数(0表示不启用),超过则停止进程
|
|
39
|
+
* @param {number} [config.stoptimeout=30] - 停止服务的超时秒数
|
|
40
|
+
* @param {number} [config.wait=1] - 脚本停止后等待重新启动的秒数
|
|
41
|
+
* @param {string} [config.nodeOptions='--harmony'] - 传递给node进程的选项
|
|
42
|
+
* @param {string} [config.scriptOptions=''] - 传递给脚本的选项
|
|
43
|
+
* @param {boolean} [config.stopparentfirst=false] - 是否先停止父进程
|
|
44
|
+
* @param {boolean} [config.abortOnError=false] - 脚本运行错误时是否退出进程
|
|
45
|
+
* @param {number} [config.grow=0.25] - 重启等待时间的增长百分比
|
|
46
|
+
* @param {string|null} [config.logpath=null] - 日志文件路径(默认与可执行文件相同目录)
|
|
47
|
+
* @param {string} [config.logmode='rotate'] - 日志模式(rotate, truncate, append)
|
|
48
|
+
* @param {string} [config.description=''] - 服务描述
|
|
49
|
+
* @param {string} [config.execPath=process.execPath] - 可执行文件路径
|
|
50
|
+
* @param {string} [config.workingDirectory=process.cwd()] - 服务进程工作目录
|
|
51
|
+
* @param {Object} [config.logOnAs] - 服务登录凭据配置
|
|
52
|
+
* @param {string} [config.logOnAs.account] - 登录账户
|
|
53
|
+
* @param {string|null} [config.logOnAs.password] - 登录密码
|
|
54
|
+
* @param {string} [config.logOnAs.domain] - 域名(默认使用COMPUTERNAME)
|
|
55
|
+
* @param {boolean} [config.logOnAs.mungeCredentialsAfterInstall=true] - 安装后是否混淆凭据
|
|
56
|
+
* @param {Object} [config.sudo] - 提升权限配置
|
|
57
|
+
* @param {boolean|null} [config.sudo.enabled] - 是否启用sudo方式提升权限
|
|
58
|
+
* @param {Object} [config.env] - 环境变量配置
|
|
59
|
+
* @param {Object} [config.logging] - 日志配置
|
|
60
|
+
* @param {boolean} [config.allowServiceLogon] - 是否允许服务登录
|
|
61
|
+
*/
|
|
62
|
+
constructor(config) {
|
|
63
|
+
super(), this.#validateConfig(config), this.#initializeProperties(config);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
*验证配置
|
|
68
|
+
* @param {Object} config - 服务配置对象
|
|
69
|
+
*/
|
|
70
|
+
#validateConfig(config) {
|
|
71
|
+
if (!config.name || !config.script) throw new Error('服务名称和脚本路径不可为空;')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 私有字段
|
|
75
|
+
#name = null;
|
|
76
|
+
#eventlog;
|
|
77
|
+
#directory;
|
|
78
|
+
|
|
79
|
+
// 名称过滤方法
|
|
80
|
+
#filterName(name) {
|
|
81
|
+
return name ? name.replace(nameRegex, '') : console.log('服务名称无效');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 初始化属性
|
|
85
|
+
#initializeProperties(config) {
|
|
86
|
+
const {
|
|
87
|
+
// 基础配置
|
|
88
|
+
name, script, maxRetries = null, maxRestarts = 3, stoptimeout = 30, wait = 1, nodeOptions = '--harmony',
|
|
89
|
+
scriptOptions = '', stopparentfirst = false, abortOnError = false, grow = 0.25, logpath = null, logmode = 'rotate',
|
|
90
|
+
description = '', execPath = process.execPath, workingDirectory = process.cwd(),
|
|
91
|
+
logOnAs = {}, sudo = {}, // 嵌套配置对象
|
|
92
|
+
env, logging, allowServiceLogon, // 其它配置
|
|
93
|
+
} = config, domain = process.env.COMPUTERNAME;
|
|
94
|
+
|
|
95
|
+
// 私有字段
|
|
96
|
+
this.#name = this.#filterName(name), this.#eventlog = null, this.#directory = script ? path.dirname(script) : null;
|
|
97
|
+
|
|
98
|
+
// 公共属性
|
|
99
|
+
this.maxRetries = maxRetries; // 服务无响应/故障之前的最大重试次数(默认忽略);
|
|
100
|
+
this.maxRestarts = maxRestarts; // 在60秒内最大重启次数(0表示不启用),超过则停止进程;
|
|
101
|
+
this.stoptimeout = stoptimeout; // 停止服务的超时时间(默认:30秒);
|
|
102
|
+
this.wait = Number(wait); // 脚本停止后等待重新启动的秒数(默认:1秒);
|
|
103
|
+
this.nodeOptions = nodeOptions; // 传递给node进程的选项;
|
|
104
|
+
this.scriptOptions = scriptOptions; // 传递给脚本的选项;
|
|
105
|
+
|
|
106
|
+
this.stopparentfirst = stopparentfirst; // 是否先停止父进程(默认:false);
|
|
107
|
+
this.abortOnError = Boolean(abortOnError); // 当遇到导致node.js脚本无法运行错误时是否退出进程(默认:false);
|
|
108
|
+
this.grow = Number(grow); // 重启等待时间的增长百分比(默认:0.25);
|
|
109
|
+
this.logpath = logpath; // 日志文件路径(默认:与可执行文件相同的目录);
|
|
110
|
+
this.logmode = logmode; // 日志模式(默认:rotate);可选值: rotate, truncate, append;
|
|
111
|
+
this.description = description; // 服务描述;
|
|
112
|
+
this.script = path.resolve(script); // 服务启动脚本的绝对路径;
|
|
113
|
+
this.execPath = path.resolve(execPath); // 启动脚本可执行文件的绝对路径;
|
|
114
|
+
this.workingdirectory = workingDirectory; // 服务进程启动工作目录的完整路径(默认:当前工作目录);
|
|
115
|
+
|
|
116
|
+
/** 服务登录凭据配置 */
|
|
117
|
+
this.logOnAs = {
|
|
118
|
+
account: undefined, password: null, domain, mungeCredentialsAfterInstall: true, ...logOnAs
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// 提升权限配置
|
|
122
|
+
this.sudo = { enabled: null, ...sudo };
|
|
123
|
+
|
|
124
|
+
/** 环境变量配置 */
|
|
125
|
+
this.env = env, this.logging = logging, this.allowServiceLogon = allowServiceLogon;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 进程名称
|
|
129
|
+
get name() {
|
|
130
|
+
return this.#name;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
set name(value) {
|
|
134
|
+
this.#name = this.#filterName(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 进程ID
|
|
138
|
+
get id() {
|
|
139
|
+
return this.name;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 可执行文件名
|
|
143
|
+
get #exe() {
|
|
144
|
+
return `${this.id}.exe`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 服务名称(用于Windows服务管理)
|
|
148
|
+
get #serviceName() {
|
|
149
|
+
return this.id;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 生成服务的XML配置
|
|
153
|
+
get #xml() {
|
|
154
|
+
// 提取需要的属性
|
|
155
|
+
const { script, scriptOptions, name, grow, wait, maxRestarts, abortOnError, stopparentfirst, maxRetries, id, nodeOptions,
|
|
156
|
+
description, logpath, execPath, logOnAs, workingdirectory, stoptimeout, logmode, env, logging, allowServiceLogon } = this,
|
|
157
|
+
wrapperArgs = ['--file', script, `--scriptoptions=${scriptOptions}`, '--log', `${name} 包装器`, '--grow', grow, '--wait', wait,
|
|
158
|
+
'--maxrestarts', maxRestarts, '--abortonerror', abortOnError ? 'y' : 'n', '--stopparentfirst', stopparentfirst,
|
|
159
|
+
...(maxRetries !== null ? ['--maxretries', maxRetries] : [])];
|
|
160
|
+
|
|
161
|
+
return generateXml({
|
|
162
|
+
name, id, nodeOptions, script: wrapper, scriptOptions, wrapperArgs, description, logpath, execPath, logOnAs, workingdirectory,
|
|
163
|
+
stopparentfirst, stoptimeout, logmode, env, logging, allowServiceLogon
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 解析脚本保存的目录
|
|
169
|
+
* @param {string} [dir] - 自定义目录路径,若不提供则使用脚本所在目录
|
|
170
|
+
* @returns {string} 服务文件的完整保存目录
|
|
171
|
+
*/
|
|
172
|
+
directory(dir) {
|
|
173
|
+
if (dir) this.#directory = path.resolve(dir);
|
|
174
|
+
return path.resolve(path.join(this.#directory, daemonDir));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#resPath(...paths) {
|
|
178
|
+
return path.resolve(this.root, ...paths);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 进程文件存储的根目录
|
|
183
|
+
* @returns {string}
|
|
184
|
+
*/
|
|
185
|
+
get root() {
|
|
186
|
+
return this.directory();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 服务事件日志记录器实例
|
|
191
|
+
* @returns {EventLogger}
|
|
192
|
+
*/
|
|
193
|
+
get log() {
|
|
194
|
+
if (this.#eventlog !== null) return this.#eventlog;
|
|
195
|
+
this.#eventlog = new EventLogger(`${this.name} 监视器`);
|
|
196
|
+
return this.#eventlog;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 服务是否存在及其状态
|
|
201
|
+
* - 0: 不存在
|
|
202
|
+
* - 1: 存在服务但无文件
|
|
203
|
+
* - 2: 存在文件但无服务
|
|
204
|
+
* - 3: 完整存在
|
|
205
|
+
* >查看定义:@see {@link exists}
|
|
206
|
+
* @returns {number} 状态码 0-3
|
|
207
|
+
*/
|
|
208
|
+
get exists() {
|
|
209
|
+
const hasFiles = fs.existsSync(this.#resPath(`${this.id}.exe`)) && fs.existsSync(this.#resPath(`${this.id}.xml`));
|
|
210
|
+
try {
|
|
211
|
+
execSync(`sc query "${this.#serviceName}"`, { stdio: 'ignore' });
|
|
212
|
+
return hasFiles ? 3 : 1;
|
|
213
|
+
} catch {
|
|
214
|
+
return hasFiles ? 2 : 0;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 将脚本安装为Windows服务
|
|
220
|
+
* >查看定义:@see {@link install}
|
|
221
|
+
* @param {string} [dir] - 服务文件将保存的目录(默认为脚本所在目录)
|
|
222
|
+
* @returns {Promise<void>}
|
|
223
|
+
* @event install - 当安装过程完成时触发
|
|
224
|
+
* @event alreadyinstalled - 如果服务已安装时触发
|
|
225
|
+
* @event invalidinstallation - 如果检测到安装但缺少必需文件时触发
|
|
226
|
+
* @event error - 在错误发生时触发
|
|
227
|
+
* @example
|
|
228
|
+
* import { Service } from '@flun/windows';
|
|
229
|
+
* const svc = new Service({
|
|
230
|
+
* name: 'Hello World', // 服务名称
|
|
231
|
+
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
232
|
+
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
233
|
+
*
|
|
234
|
+
* // 传递给node进程的选项
|
|
235
|
+
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
236
|
+
* });
|
|
237
|
+
*
|
|
238
|
+
* // 监听安装完成事件
|
|
239
|
+
* svc.on('install', ()=>{
|
|
240
|
+
* svc.start();
|
|
241
|
+
* });
|
|
242
|
+
*
|
|
243
|
+
* svc.install();
|
|
244
|
+
*/
|
|
245
|
+
async install(dir) {
|
|
246
|
+
// 检查是否已安装
|
|
247
|
+
if (this.exists === 3) return console.log('安装跳过,服务已经存在;'), this.emit('alreadyinstalled');
|
|
248
|
+
|
|
249
|
+
const targetDir = this.directory(dir);
|
|
250
|
+
if (!fs.existsSync(targetDir)) await mkdirAsync(targetDir, { recursive: true });
|
|
251
|
+
try {
|
|
252
|
+
// 写入配置文件&创建可执行文件
|
|
253
|
+
await writeFileAsync(this.#resPath(`${this.id}.xml`), this.#xml);
|
|
254
|
+
await new Promise((resolve, reject) => {
|
|
255
|
+
createExe(this.id, targetDir, error => error ? reject(error) : resolve());
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// 执行安装命令
|
|
259
|
+
await this.#execute(`"${this.#resPath(this.#exe)}" install`), await this.#sleep(2), this.emit('install');
|
|
260
|
+
} catch (error) {
|
|
261
|
+
this.emit('error', error);
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 卸载服务
|
|
268
|
+
* >查看定义:@see {@link uninstall}
|
|
269
|
+
* @param {number} [waitTime=2] - 等待winsw.exe完成卸载命令的秒数
|
|
270
|
+
* @returns {Promise<void>}
|
|
271
|
+
* @event uninstall - 当卸载过程完成时触发
|
|
272
|
+
* @event alreadyuninstalled - 如果服务已卸载时触发
|
|
273
|
+
* @example
|
|
274
|
+
* import { Service } from '@flun/windows';
|
|
275
|
+
* const svc = new Service({
|
|
276
|
+
* name: 'Hello World', // 服务名称
|
|
277
|
+
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
278
|
+
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
279
|
+
*
|
|
280
|
+
* // 传递给node进程的选项
|
|
281
|
+
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
282
|
+
* });
|
|
283
|
+
* svc.on('uninstall', ()=>{
|
|
284
|
+
* console.log('卸载完成');
|
|
285
|
+
* console.log('服务是否存在:', svc.exists);
|
|
286
|
+
* // svc.exists 返回值说明:
|
|
287
|
+
* // 0: 服务和相关文件没有
|
|
288
|
+
* // 1: 服务已注册但相关文件不存在(异常情况)
|
|
289
|
+
* // 2: 服务未注册但相关文件存在(文件残留)
|
|
290
|
+
* // 3: 服务已注册且相关文件存在(正常状态)
|
|
291
|
+
* });
|
|
292
|
+
*
|
|
293
|
+
* svc.uninstall();
|
|
294
|
+
*/
|
|
295
|
+
async uninstall(waitTime = 2) {
|
|
296
|
+
if (!this.exists) return console.log('卸载已跳过,服务已经不在;'), this.emit('alreadyuninstalled');
|
|
297
|
+
|
|
298
|
+
const uninstaller = async () => {
|
|
299
|
+
await this.#execute(`"${this.#resPath(this.#exe)}" uninstall`), await this.#sleep(waitTime);
|
|
300
|
+
try {
|
|
301
|
+
await fs.promises.rm(this.root, { recursive: true, force: true }), await this.#sleep(1);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(`删除目录失败: ${error.message}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.emit('uninstall');
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
this.once('stop', uninstaller), this.once('alreadystopped', uninstaller), await this.stop();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 启动现有服务
|
|
314
|
+
* >查看定义:@see {@link start}
|
|
315
|
+
* @returns {Promise<void>}
|
|
316
|
+
* @event start - 当服务启动时触发
|
|
317
|
+
* @example
|
|
318
|
+
* import { Service } from '@flun/windows';
|
|
319
|
+
* const svc = new Service({
|
|
320
|
+
* name: 'Hello World', // 服务名称
|
|
321
|
+
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
322
|
+
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
323
|
+
*
|
|
324
|
+
* // 传递给node进程的选项
|
|
325
|
+
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
326
|
+
* });
|
|
327
|
+
* svc.on('start', ()=>{
|
|
328
|
+
* console.log('服务已启动');
|
|
329
|
+
* });
|
|
330
|
+
*
|
|
331
|
+
* svc.start();
|
|
332
|
+
*/
|
|
333
|
+
async start() {
|
|
334
|
+
if (this.exists !== 3) throw new Error(`启动 ${this.name} 服务条件缺失(exists返回码:${this.exists});`);
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const { stderr } = await this.#execute(`NET START "${this.#serviceName}"`);
|
|
338
|
+
if (!stderr) return this.emit('start'); // 如果没有错误,触发成功事件
|
|
339
|
+
throw new Error(stderr);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error.message.includes('already been started')) return this.log.warn('尝试启动服务失败,因为服务已经在运行;');
|
|
342
|
+
this.emit('error', error);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 停止服务
|
|
348
|
+
* >查看定义:@see {@link stop}
|
|
349
|
+
* @returns {Promise<void>}
|
|
350
|
+
* @event stop - 当服务停止时触发
|
|
351
|
+
* @event alreadystopped - 当服务已经停止时触发
|
|
352
|
+
* @example
|
|
353
|
+
* import { Service } from '@flun/windows';
|
|
354
|
+
* const svc = new Service({
|
|
355
|
+
* name: 'Hello World', // 服务名称
|
|
356
|
+
* description: 'nodejs.org 示例服务器', // 服务描述
|
|
357
|
+
* script: 'C:\\path\\to\\helloworld.js',// 启动服务的入口脚本路径
|
|
358
|
+
*
|
|
359
|
+
* // 传递给node进程的选项
|
|
360
|
+
* nodeOptions: [ '--harmony', '--max-old-space-size=4096' ]
|
|
361
|
+
* });
|
|
362
|
+
* svc.on('stop', ()=>{
|
|
363
|
+
* console.log('服务已停止');
|
|
364
|
+
* });
|
|
365
|
+
*
|
|
366
|
+
* svc.stop();
|
|
367
|
+
*/
|
|
368
|
+
async stop() {
|
|
369
|
+
try {
|
|
370
|
+
await this.#execute(`NET STOP "${this.#serviceName}"`), this.emit('stop');
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (error.code === 2) return this.log.warn('服务未运行或已停止;'), this.emit('alreadystopped');
|
|
373
|
+
this.emit('error', error);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 重启现有服务
|
|
379
|
+
* >查看定义:@see {@link restart}
|
|
380
|
+
* @returns {Promise<void>}
|
|
381
|
+
* @example
|
|
382
|
+
* import { Service } from '@flun/windows';
|
|
383
|
+
* const svc = new Service({
|
|
384
|
+
* name: 'Hello World',
|
|
385
|
+
* script: 'C:\\path\\to\\helloworld.js'
|
|
386
|
+
* });
|
|
387
|
+
*
|
|
388
|
+
* svc.restart();
|
|
389
|
+
*/
|
|
390
|
+
async restart() {
|
|
391
|
+
this.once('stop', () => this.start()), await this.stop();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* 使用提升的权限执行命令
|
|
396
|
+
* @param {string} cmd - 要执行的命令
|
|
397
|
+
* @param {Object} [options={}] - 额外执行选项
|
|
398
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
399
|
+
*/
|
|
400
|
+
async #execute(cmd, options = {}) {
|
|
401
|
+
return new Promise((resolve, reject) => {
|
|
402
|
+
// 检查sudo是否为真
|
|
403
|
+
const executor = this.sudo.enabled ? (cmd, opts, cb) => binSudo(cmd, opts, cb) : elevate;
|
|
404
|
+
executor(cmd, { ...options, shell: true, windowsHide: true }, (error, stdout, stderr) => {
|
|
405
|
+
if (error) {
|
|
406
|
+
if (isPermissionError(error.message)) reject(new Error('权限被拒绝, 请以管理员身份重新运行此脚本;'));
|
|
407
|
+
else console.error(error.toString()), reject(error);
|
|
408
|
+
}
|
|
409
|
+
else resolve({ stdout, stderr });
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 睡眠指定秒数
|
|
416
|
+
* @param {number} seconds - 睡眠秒数
|
|
417
|
+
* @returns {Promise<void>}
|
|
418
|
+
*/
|
|
419
|
+
#sleep = seconds => new Promise(r => setTimeout(r, seconds * 1000));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
422
|
export { Service };
|