@flun/windows 2.0.10 → 3.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 +12 -3
- package/README.md +19 -20
- package/index.d.ts +3 -2
- package/lib/daemon.js +24 -17
- package/lib/eventlog.js +3 -4
- package/lib/shared.js +8 -1
- package/package.json +1 -1
- package/sevWin.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
# 变更日志
|
|
2
|
-
## [
|
|
3
|
-
###
|
|
4
|
-
-
|
|
2
|
+
## [3.0.0] - 2026-5-28 21:40
|
|
3
|
+
### 破坏性变更
|
|
4
|
+
- 移除 `sudo: { enabled: false }` 配置项,现在内部自动检查 sudo 可用性并默认使用提权方式,不再支持显式关闭。
|
|
5
|
+
|
|
6
|
+
### 修复
|
|
7
|
+
- 修复 Windows 服务环境下 `os.homedir()` 返回系统目录(如 `C:\Windows\System32\config\systemprofile`)而非当前用户目录的问题
|
|
8
|
+
现在在生成服务 XML 配置时,会自动将安装服务时的当前用户 `USERPROFILE` 注入到服务进程的环境变量中,使得 `os.homedir()` 正确返回用户目录,从而解决依赖用户目录的证书续期等业务路径错误的问题;
|
|
9
|
+
|
|
10
|
+
### 优化
|
|
11
|
+
- 简化 `#xml` getter 中的环境变量处理逻辑,自动合并用户自定义的 `env` 配置,仅在未显式设置 `USERPROFILE` 时追加正确的用户目录;
|
|
12
|
+
- 简化内部提权判断逻辑,统一使用 `sudo`作为默认(若可用), `elevate` 次之作为提权方式。
|
|
13
|
+
- 删除了一些没有使用的导出等一些细节优化;
|
package/README.md
CHANGED
|
@@ -66,7 +66,6 @@ const svc = new Service({
|
|
|
66
66
|
//, logmode: 'rotate' // 日志模式('reset', 'roll', 'append')
|
|
67
67
|
//, execPath: process.execPath // Node.js可执行文件路径(默认当前Node)
|
|
68
68
|
//, logOnAs: {} // 服务运行的用户凭据('domain'、'account'、'password'和'mungeCredentialsAfterInstall')
|
|
69
|
-
//, sudo: {enabled:false} // 提升权限配置(是否在Windows启用了sudo,默认没有)
|
|
70
69
|
//, env: undefined // 环境变量(默认:undefined)
|
|
71
70
|
//, logging: null // 日志配置(默认:mode = 'append', pattern = 'yyyMMdd', sizeThreshold = 10240, keepFiles = 8)
|
|
72
71
|
//, allowServiceLogon: undefined // 允许服务登录(默认undefined)
|
|
@@ -108,26 +107,28 @@ const svc1 = new Service({
|
|
|
108
107
|
|
|
109
108
|
### 环境变量设置
|
|
110
109
|
|
|
110
|
+
`@flun/windows` 会自动向服务进程注入 `USERPROFILE` 环境变量,其值为**安装服务时当前登录用户的主目录**(例如 `C:\Users\你的用户名`;
|
|
111
|
+
这确保了服务中调用 `os.homedir()` 能返回正确的用户目录,即使服务以 `LocalSystem` 等系统账户运;
|
|
112
|
+
|
|
113
|
+
**你通常不需要手动设置 `USERPROFILE`**,如需添加其他自定义环境变量,可按以下方式:
|
|
114
|
+
|
|
111
115
|
```js
|
|
112
116
|
import { Service } from '@flun/windows';
|
|
113
|
-
const
|
|
114
|
-
|
|
117
|
+
const svc = new Service({
|
|
118
|
+
name: 'My Service',
|
|
119
|
+
script: 'C:\\app\\server.js',
|
|
115
120
|
env: {
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
MY_CUSTOM_VAR: 'some value',
|
|
122
|
+
ANOTHER_VAR: 'hello'
|
|
118
123
|
}
|
|
119
124
|
});
|
|
120
|
-
|
|
121
|
-
// 或多个变量
|
|
125
|
+
// 也支持数组形式
|
|
122
126
|
const svc2 = new Service({
|
|
123
|
-
...,
|
|
124
|
-
env: [
|
|
125
|
-
name:
|
|
126
|
-
value:
|
|
127
|
-
|
|
128
|
-
name: "TEMP",
|
|
129
|
-
value: path.join(process.env["USERPROFILE"],"/temp")
|
|
130
|
-
}]
|
|
127
|
+
...,
|
|
128
|
+
env: [
|
|
129
|
+
{ name: 'VAR1', value: 'value1' },
|
|
130
|
+
{ name: 'VAR2', value: 'value2' }
|
|
131
|
+
]
|
|
131
132
|
});
|
|
132
133
|
```
|
|
133
134
|
|
|
@@ -154,10 +155,8 @@ svc.logOnAs.domain = 'mydomain.local';
|
|
|
154
155
|
svc.logOnAs.account = 'username';
|
|
155
156
|
svc.logOnAs.password = 'password';
|
|
156
157
|
svc.logOnAs.mungeCredentialsAfterInstall: true // 默认
|
|
157
|
-
|
|
158
|
-
// 显示启用 sudo 提权 (注意!!需要在系统中启用了sudo,然后在这里设置为true,才有效)
|
|
159
|
-
svc.sudo.enabled = true;
|
|
160
158
|
```
|
|
159
|
+
> 💡 **提示**:即使你使用默认的 `LocalSystem` 账户运行服务,`@flun/windows` 也会通过自动注入 `USERPROFILE` 环境变量,使你的应用能够正确获取当前登录用户的目录(例如读取用户目录下的配置文件);因此,你不需要为了修正 `os.homedir()` 而特意配置 `logOnAs` 账户和密码;
|
|
161
160
|
|
|
162
161
|
### 服务卸载
|
|
163
162
|
|
|
@@ -199,7 +198,7 @@ const svc = new Service({
|
|
|
199
198
|
### 服务实现原理
|
|
200
199
|
|
|
201
200
|
使用 [winsw]为每个服务生成独立的 `.exe` 和 `.xml` 配置文件,存储在脚本所在目录的 `daemon` 子文件夹中;服务日志可通过 Windows 事件查看器查看;
|
|
202
|
-
|
|
201
|
+
此外,服务启动时会自动设置 `USERPROFILE` 环境变量为安装服务时当前用户的用户目录,保证路径相关 API 的行为与预期一致;
|
|
203
202
|
---
|
|
204
203
|
|
|
205
204
|
# 事件日志系统
|
|
@@ -342,7 +341,7 @@ kill(进程PID, () => {
|
|
|
342
341
|
|
|
343
342
|
3. **回调函数未执行**:确保正确传递回调函数,如果不需要回调,可以不传但无法获取执行结果
|
|
344
343
|
|
|
345
|
-
4. **sudo
|
|
344
|
+
4. **sudo 函数需要在系统中设置并启用sudo**
|
|
346
345
|
|
|
347
346
|
## 服务管理常见问题
|
|
348
347
|
|
package/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { elevate, sudo, isAdminUser, platform } from './lib/binaries.js';
|
|
|
2
2
|
import { kill, list } from './lib/cmd.js';
|
|
3
3
|
import { Service } from './lib/daemon.js';
|
|
4
4
|
import { EventLogger } from './lib/eventlog.js';
|
|
5
|
-
import { exec, execSync, fork, promisify, path, fs, EventEmitter, isPermissionError, getDirname } from './lib/shared.js';
|
|
5
|
+
import { exec, execSync, fork, promisify, execPromise, path, fs, EventEmitter, isPermissionError, getDirname } from './lib/shared.js';
|
|
6
6
|
import { generateXml, createExe } from './lib/winsw.js';
|
|
7
7
|
|
|
8
8
|
// =================================== lib/binaries.js ===================================
|
|
@@ -70,11 +70,12 @@ declare module './lib/eventlog.js' {
|
|
|
70
70
|
* path, fs; // Node.js内置模块,提供文件路径处理、文件系统操作等功能
|
|
71
71
|
* class EventEmitter{}; // Node.js内置模块,提供事件驱动编程的功能,允许对象之间进行事件通信
|
|
72
72
|
* // 自定义函数:
|
|
73
|
+
* execPromise(); // 基于exec函数的Promise版本,用于执行命令并以Promise方式获取结果
|
|
73
74
|
* isPermissionError(); // 检查错误对象是否表示权限错误的函数,用于判断操作失败是否由于权限不足引起
|
|
74
75
|
* getDirname(); // 获取当前模块目录路径的函数,用于在ES模块环境中替代__dirname变量的功能
|
|
75
76
|
* ```
|
|
76
77
|
* >查看定义:@see {@link exec}、{@link execSync}、{@link fork}、{@link promisify}、{@link path}、{@link fs}、{@link EventEmitter}
|
|
77
|
-
* - 自定义函数:{@link isPermissionError}、{@link getDirname}
|
|
78
|
+
* - 自定义函数:{@link execPromise}、{@link isPermissionError}、{@link getDirname}
|
|
78
79
|
*/
|
|
79
80
|
declare module './lib/shared.js' {
|
|
80
81
|
export * from './lib/shared.js';
|
package/lib/daemon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { elevate, sudo
|
|
1
|
+
import { execSync, execPromise, promisify, path, fs, EventEmitter, isPermissionError, getDirname } from './shared.js';
|
|
2
|
+
import { elevate, sudo } from './binaries.js';
|
|
3
3
|
import { generateXml, createExe } from './winsw.js';
|
|
4
4
|
import { EventLogger } from './eventlog.js';
|
|
5
5
|
|
|
@@ -53,8 +53,6 @@ class Service extends EventEmitter {
|
|
|
53
53
|
* @param {string|null} [config.logOnAs.password] - 登录密码
|
|
54
54
|
* @param {string} [config.logOnAs.domain] - 域名(默认使用COMPUTERNAME)
|
|
55
55
|
* @param {boolean} [config.logOnAs.mungeCredentialsAfterInstall=true] - 安装后是否混淆凭据
|
|
56
|
-
* @param {Object} [config.sudo] - 提升权限配置
|
|
57
|
-
* @param {boolean|null} [config.sudo.enabled] - 是否启用sudo方式提升权限
|
|
58
56
|
* @param {Object} [config.env] - 环境变量配置
|
|
59
57
|
* @param {Object} [config.logging] - 日志配置
|
|
60
58
|
* @param {boolean} [config.allowServiceLogon] - 是否允许服务登录
|
|
@@ -88,7 +86,7 @@ class Service extends EventEmitter {
|
|
|
88
86
|
name, script, maxRetries = null, maxRestarts = 3, stoptimeout = 30, wait = 1, nodeOptions = '--harmony',
|
|
89
87
|
scriptOptions = '', stopparentfirst = false, abortOnError = false, grow = 0.25, logpath = null, logmode = 'rotate',
|
|
90
88
|
description = '', execPath = process.execPath, workingDirectory = process.cwd(),
|
|
91
|
-
logOnAs = {}
|
|
89
|
+
logOnAs = {},// 嵌套配置对象
|
|
92
90
|
env, logging, allowServiceLogon, // 其它配置
|
|
93
91
|
} = config, domain = process.env.COMPUTERNAME;
|
|
94
92
|
|
|
@@ -118,9 +116,6 @@ class Service extends EventEmitter {
|
|
|
118
116
|
account: undefined, password: null, domain, mungeCredentialsAfterInstall: true, ...logOnAs
|
|
119
117
|
};
|
|
120
118
|
|
|
121
|
-
// 提升权限配置
|
|
122
|
-
this.sudo = { enabled: null, ...sudo };
|
|
123
|
-
|
|
124
119
|
/** 环境变量配置 */
|
|
125
120
|
this.env = env, this.logging = logging, this.allowServiceLogon = allowServiceLogon;
|
|
126
121
|
}
|
|
@@ -151,16 +146,22 @@ class Service extends EventEmitter {
|
|
|
151
146
|
|
|
152
147
|
// 生成服务的XML配置
|
|
153
148
|
get #xml() {
|
|
154
|
-
// 提取需要的属性
|
|
155
149
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
150
|
+
description, logpath, execPath, logOnAs, workingdirectory, stoptimeout, logmode, env, logging, allowServiceLogon } = this;
|
|
151
|
+
|
|
152
|
+
// 构建环境变量数组,自动添加 USERPROFILE(如果未显式设置)
|
|
153
|
+
let envList = [];
|
|
154
|
+
if (env) envList.push(...(Array.isArray(env) ? env : [env]));
|
|
155
|
+
if (process.env.USERPROFILE && !envList.some(e => e.name === 'USERPROFILE'))
|
|
156
|
+
envList.push({ name: 'USERPROFILE', value: process.env.USERPROFILE });
|
|
157
|
+
|
|
158
|
+
const wrapperArgs = ['--file', script, `--scriptoptions=${scriptOptions}`, '--log', `${name} 包装器`, '--grow', grow, '--wait', wait,
|
|
159
|
+
'--maxrestarts', maxRestarts, '--abortonerror', abortOnError ? 'y' : 'n', '--stopparentfirst', stopparentfirst,
|
|
160
|
+
...(maxRetries !== null ? ['--maxretries', maxRetries] : [])];
|
|
160
161
|
|
|
161
162
|
return generateXml({
|
|
162
163
|
name, id, nodeOptions, script: wrapper, scriptOptions, wrapperArgs, description, logpath, execPath, logOnAs, workingdirectory,
|
|
163
|
-
stopparentfirst, stoptimeout, logmode, env, logging, allowServiceLogon
|
|
164
|
+
stopparentfirst, stoptimeout, logmode, env: envList, logging, allowServiceLogon
|
|
164
165
|
});
|
|
165
166
|
}
|
|
166
167
|
|
|
@@ -398,12 +399,18 @@ class Service extends EventEmitter {
|
|
|
398
399
|
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
399
400
|
*/
|
|
400
401
|
async #execute(cmd, options = {}) {
|
|
402
|
+
// 检查 sudo 是否启用,并根据可用性选择提升方法
|
|
403
|
+
const isSudoAvailableOnWindows = async () => {
|
|
404
|
+
try {
|
|
405
|
+
await execPromise('sudo --version', { shell: true });
|
|
406
|
+
return true;
|
|
407
|
+
} catch { return false }
|
|
408
|
+
}, sudoAvailable = await isSudoAvailableOnWindows(), executor = sudoAvailable ? sudo : elevate;
|
|
409
|
+
|
|
401
410
|
return new Promise((resolve, reject) => {
|
|
402
|
-
// 检查sudo是否为真
|
|
403
|
-
const executor = this.sudo.enabled ? (cmd, opts, cb) => binSudo(cmd, opts, cb) : elevate;
|
|
404
411
|
executor(cmd, { ...options, shell: true, windowsHide: true }, (error, stdout, stderr) => {
|
|
405
412
|
if (error) {
|
|
406
|
-
if (isPermissionError(error.message)) reject(new Error('
|
|
413
|
+
if (isPermissionError(error.message)) reject(new Error('权限被拒绝,请以管理员身份重新运行此脚本;'));
|
|
407
414
|
else console.error(error.toString()), reject(error);
|
|
408
415
|
}
|
|
409
416
|
else resolve({ stdout, stderr });
|
package/lib/eventlog.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execPromise, execSync, isPermissionError } from './shared.js';
|
|
2
2
|
import { elevate } from './binaries.js';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
eventLogs = ['APPLICATION', 'SYSTEM', 'SECURITY'], validTypes = ['ERROR', 'WARNING', 'INFORMATION', 'SUCCESSAUDIT', 'FAILUREAUDIT'];
|
|
4
|
+
const eventLogs = ['APPLICATION', 'SYSTEM', 'SECURITY'], validTypes = ['ERROR', 'WARNING', 'INFORMATION', 'SUCCESSAUDIT', 'FAILUREAUDIT'];
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* 事件日志记录器类,用于向Windows事件查看器写入日志
|
|
@@ -136,7 +135,7 @@ class EventLogger {
|
|
|
136
135
|
|
|
137
136
|
// 执行命令
|
|
138
137
|
try {
|
|
139
|
-
await
|
|
138
|
+
await execPromise(command), callback?.();
|
|
140
139
|
} catch (error) {
|
|
141
140
|
if (isPermissionError(error?.message)) await this.#elevateCommand(command, callback);
|
|
142
141
|
else {
|
package/lib/shared.js
CHANGED
|
@@ -9,6 +9,13 @@ import fs from 'fs';
|
|
|
9
9
|
import { EventEmitter } from 'events';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* 将 child_process.exec 包装成 Promise
|
|
14
|
+
* >查看定义:@see {@link execPromise}
|
|
15
|
+
* @type {function(string, Object): Promise<{ stdout: string, stderr: string }>}
|
|
16
|
+
*/
|
|
17
|
+
const execPromise = promisify(exec);
|
|
18
|
+
|
|
12
19
|
/**
|
|
13
20
|
* 检查错误是否为权限不足的错误
|
|
14
21
|
* >查看定义:@see {@link isPermissionError}
|
|
@@ -31,4 +38,4 @@ const isPermissionError = error => {
|
|
|
31
38
|
*/
|
|
32
39
|
const getDirname = metaUrl => path.dirname(fileURLToPath(metaUrl));
|
|
33
40
|
|
|
34
|
-
export { exec, execSync, fork, promisify, path, fs, EventEmitter, isPermissionError, getDirname };
|
|
41
|
+
export { exec, execSync, fork, execPromise, promisify, path, fs, EventEmitter, isPermissionError, getDirname };
|
package/package.json
CHANGED
package/sevWin.js
CHANGED
|
@@ -7,7 +7,6 @@ const serviceName = 'TestApp', scriptPath = 'D:\\test\\dev.js', // 请根据实
|
|
|
7
7
|
script: scriptPath,
|
|
8
8
|
nodeOptions: ['--harmony', '--max-old-space-size=4096'],
|
|
9
9
|
env: { name: "NODE_ENV", value: "production" },
|
|
10
|
-
// sudo: { enabled: true }
|
|
11
10
|
});
|
|
12
11
|
|
|
13
12
|
const installService = () => {
|