@flun/windows 2.0.1

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.
@@ -0,0 +1,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, "%%"), vbs = readFileSafe(vbsPath).replace(/{command}/g, escapedCmd);
59
+
60
+ // 写入VBS文件
61
+ try {
62
+ fs.writeFileSync(vbsFile, vbs, 'utf8');
63
+ } catch (writeError) {
64
+ return p.callback(writeError, '', '');
65
+ }
66
+
67
+ exec(`wscript.exe "${vbsFile}"`, { timeout: 20000 }, error => {
68
+ setTimeout(() => {
69
+ const output = readFileSafe(outFile).replace(/\r\n$|\n$/, '').trim();
70
+
71
+ // 清理临时文件
72
+ [batFile, cmdFile, outFile, vbsFile].forEach(f => {
73
+ try { fs.unlinkSync(f) } catch (e) { }
74
+ });
75
+
76
+ if (error) return p.callback(error, '', '');
77
+ p.callback(null, output, '');
78
+ }, 1600);
79
+ });
80
+ };
81
+
82
+ /**
83
+ * 使用 sudo 方式提升权限(与 elevate 相似但体验更好)
84
+ * >查看定义:@see {@link sudo}
85
+ * @param {string} cmd - 要使用提升权限执行的命令
86
+ * @param {Object} [options] - 传递给 child_process.exec 的选项
87
+ * @param {string} [options.cwd] - 工作目录
88
+ * @param {number} [options.timeout] - 超时时间(毫秒)
89
+ * @param {(error: Error|null, stdout: string, stderr: string) => void} [callback] - 执行完成后的回调函数
90
+ * @returns {void}
91
+ * @example
92
+ * import { sudo } from '@flun/windows';
93
+ *
94
+ * // 基本用法 - 命令、选项和回调
95
+ * sudo('echo "Hello World" && whoami', {}, (error, stdout, stderr) => {
96
+ * if (error) {
97
+ * console.error('执行失败:', error);
98
+ * } else {
99
+ * console.log('执行成功,输出:', stdout);
100
+ * console.log('错误输出:', stderr);
101
+ * }
102
+ * });
103
+ *
104
+ * // 带选项参数
105
+ * sudo('echo "Hello World" && whoami', {cwd:'C:\\'}, callback);
106
+ *
107
+ * sudo('echo "Hello World" && whoami' ); // 无回调
108
+ */
109
+ const sudo = (cmd, options, callback) => {
110
+ const p = _params(options, callback);
111
+ exec(`sudo ${cmd}`, p.options, p.callback);
112
+ };
113
+
114
+ /**
115
+ * 检查当前用户是否拥有管理员权限
116
+ * >查看定义:@see {@link isAdminUser}
117
+ * @param {(isAdmin: boolean) => void} callback - 回调函数,接收布尔值表示是否为管理员
118
+ * @returns {void}
119
+ * @example
120
+ * import { isAdminUser } from '@flun/windows';
121
+ * isAdminUser(isAdmin => {
122
+ * if (isAdmin) console.log('当前用户是管理员');
123
+ * else console.log('当前用户不是管理员');
124
+ * });
125
+ */
126
+ const isAdminUser = callback => {
127
+ exec('NET SESSION', (err, so, se) => {
128
+ if (se.length !== 0) elevate('NET SESSION', (_err, _so, _se) => callback(_se.length === 0));
129
+ else callback(true);
130
+ });
131
+ };
132
+
133
+ export { elevate, sudo, isAdminUser, platform };
package/lib/cmd.js ADDED
@@ -0,0 +1,54 @@
1
+ import { exec } from './shared.js';
2
+
3
+ /**
4
+ * 结束指定进程
5
+ * >查看定义:@see {@link kill}
6
+ * @param {number} pid - 进程PID
7
+ * @param {(error: Error|null, stdout: string, stderr: string) => void} [callback] - 执行完成后的回调函数
8
+ * @param {boolean} [force=false] - 是否强制结束进程(默认: false)
9
+ * @returns {void}
10
+ * @example
11
+ * import { kill } from '@flun/windows';
12
+ * kill(进程PID, () => {
13
+ * console.log('进程已终止');
14
+ * });
15
+ */
16
+ const kill = (pid, callback, force = false) => {
17
+ if (!pid) throw new Error('PID是kill操作必需的参数。');
18
+ if (isNaN(pid)) throw new Error('PID必须为数字。');
19
+ if (typeof callback !== 'function') callback = function () { };
20
+ exec(`taskkill /PID ${pid}${force == true ? ' /f' : ''}`, callback);
21
+ };
22
+
23
+ /**
24
+ * 列出服务器上正在运行的进程
25
+ * >查看定义:@see {@link list}
26
+ * @param {(processes: Array<Record<string, string>>) => void} callback - 回调函数,接收进程对象数组
27
+ * @param {boolean} [verbose=false] - 是否显示详细信息(默认: false)
28
+ * @returns {void}
29
+ * @example
30
+ * import { list } from '@flun/windows';
31
+ * list(processes => {
32
+ * console.log(processes);
33
+ * }, true); // true 显示详细信息
34
+ */
35
+ const list = (callback, verbose = false) => {
36
+ exec(`tasklist /FO CSV${verbose ? ' /V' : ''}`, (err, stdout, stderr) => {
37
+ const lines = stdout.split('\r\n'), processes = [],
38
+ commaQuoteRegex = /",/g, quoteRegex = /['"]/g, whitespaceRegex = /\s/g; // 预编译正则表达式(匹配 CSV 格式)
39
+ let headers = null;
40
+ for (const line of lines.slice(1, -1)) {
41
+ if (!line.trim()) continue; // 跳过空行
42
+ // 替换 CSV 中的字段分隔符,移除所有引号
43
+ let record = line.replace(commaQuoteRegex, '";').replace(quoteRegex, '').split(';');
44
+ if (!headers) headers = record.map(header => header.replace(whitespaceRegex, ''));
45
+ else {
46
+ const processObj = {};
47
+ record.forEach((value, index) => processObj[headers[index]] = value.replace(quoteRegex, ''));
48
+ processes.push(processObj);
49
+ }
50
+ }
51
+ callback(processes);
52
+ });
53
+ }
54
+ export { kill, list };
package/lib/daemon.js ADDED
@@ -0,0 +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
+
422
+ export { Service };