@oxiaom/adoremix 1.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/bin/adoremix.js +11 -0
- package/bin/postinstall-hint.js +14 -0
- package/package.json +50 -0
- package/src/cli.js +263 -0
- package/src/config/defaults.js +98 -0
- package/src/config/index.js +102 -0
- package/src/config/ini.js +60 -0
- package/src/config/wizard.js +124 -0
- package/src/index.js +7 -0
- package/src/install.js +173 -0
- package/src/logger.js +29 -0
- package/src/paths.js +94 -0
- package/src/runner/index.js +6 -0
- package/src/runner/pid.js +65 -0
- package/src/runner/signals.js +15 -0
- package/src/runner/spawn.js +153 -0
- package/src/service/index.js +15 -0
- package/src/service/linux.js +139 -0
- package/src/service/windows.js +88 -0
- package/templates/adoremix.service.tmpl +19 -0
- package/templates/winservice.js.tmpl +66 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const prompts = require('prompts');
|
|
5
|
+
const logger = require('../logger');
|
|
6
|
+
|
|
7
|
+
function guessLocalIP() {
|
|
8
|
+
const ifaces = os.networkInterfaces();
|
|
9
|
+
for (const name of Object.keys(ifaces)) {
|
|
10
|
+
for (const it of ifaces[name]) {
|
|
11
|
+
if (it.family === 'IPv4' && !it.internal) return it.address;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return '127.0.0.1';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const QUESTIONS = [
|
|
18
|
+
{
|
|
19
|
+
type: 'text',
|
|
20
|
+
name: 'LocalIP',
|
|
21
|
+
message: '本机对外 IP(用于 Meida_ip / Fip / LocalIP)',
|
|
22
|
+
initial: () => guessLocalIP()
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: 'text',
|
|
26
|
+
name: 'task_ip',
|
|
27
|
+
message: 'MySQL 主机',
|
|
28
|
+
initial: '127.0.0.1'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'number',
|
|
32
|
+
name: 'sqltask_port',
|
|
33
|
+
message: 'MySQL 端口',
|
|
34
|
+
initial: 3307
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'text',
|
|
38
|
+
name: 'task_username',
|
|
39
|
+
message: 'MySQL 用户名',
|
|
40
|
+
initial: 'root'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'password',
|
|
44
|
+
name: 'task_passwd',
|
|
45
|
+
message: 'MySQL 密码'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
name: 'basename',
|
|
50
|
+
message: 'MySQL 数据库名',
|
|
51
|
+
initial: 'adore'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'confirm',
|
|
55
|
+
name: 'EnableRedis',
|
|
56
|
+
message: '启用 Redis?',
|
|
57
|
+
initial: false
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: (prev) => prev ? 'text' : null,
|
|
61
|
+
name: 'RedisIP',
|
|
62
|
+
message: 'Redis 主机',
|
|
63
|
+
initial: '127.0.0.1'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: (prev, values) => values.EnableRedis ? 'number' : null,
|
|
67
|
+
name: 'RedisPort',
|
|
68
|
+
message: 'Redis 端口',
|
|
69
|
+
initial: 6379
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: (prev, values) => values.EnableRedis ? 'password' : null,
|
|
73
|
+
name: 'RedisAUTH',
|
|
74
|
+
message: 'Redis 密码(无可留空)'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: 'confirm',
|
|
78
|
+
name: '_enableTTS',
|
|
79
|
+
message: '启用讯飞 TTS?',
|
|
80
|
+
initial: true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: (prev) => prev ? 'text' : null,
|
|
84
|
+
name: 'ttsxfAPPID',
|
|
85
|
+
message: '讯飞 APPID'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
type: (prev, values) => values._enableTTS ? 'text' : null,
|
|
89
|
+
name: 'ttsxfAPISecret',
|
|
90
|
+
message: '讯飞 APISecret'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: (prev, values) => values._enableTTS ? 'text' : null,
|
|
94
|
+
name: 'ttsxfAPIKey',
|
|
95
|
+
message: '讯飞 APIKey'
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
async function run(current) {
|
|
100
|
+
const answers = {};
|
|
101
|
+
const initial = Object.assign({}, current || {});
|
|
102
|
+
|
|
103
|
+
const onCancel = () => {
|
|
104
|
+
logger.warn('已取消');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (const q of QUESTIONS) {
|
|
109
|
+
let q2 = q;
|
|
110
|
+
if (typeof q.initial === 'function' && q.type === 'text') {
|
|
111
|
+
q2 = Object.assign({}, q, { initial: q.initial(answers) });
|
|
112
|
+
} else if (initial[q.name] !== undefined) {
|
|
113
|
+
q2 = Object.assign({}, q, { initial: initial[q.name] });
|
|
114
|
+
}
|
|
115
|
+
const res = await prompts(q2, { onCancel });
|
|
116
|
+
if (res[q.name] !== undefined) {
|
|
117
|
+
answers[q.name] = res[q.name];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
delete answers._enableTTS;
|
|
121
|
+
return answers;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { run, guessLocalIP };
|
package/src/index.js
ADDED
package/src/install.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const fse = require('fs-extra');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const paths = require('./paths');
|
|
9
|
+
const logger = require('./logger');
|
|
10
|
+
|
|
11
|
+
const EMPTY_DIRS = ['var', 'logs', 'dmp', 'tty', 'temp'];
|
|
12
|
+
|
|
13
|
+
const NODE_HELPER_DEPS = {
|
|
14
|
+
'axios': '^1.5.0',
|
|
15
|
+
'crypto-js': '^4.1.1',
|
|
16
|
+
'log4node': '^0.1.6',
|
|
17
|
+
'mysql': '^2.18.1',
|
|
18
|
+
'request': '^2.88.2',
|
|
19
|
+
'sqlite': '^4.2.0',
|
|
20
|
+
'ws': '^8.13.0'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function isWorkdirInitialized(workdir) {
|
|
24
|
+
return fs.existsSync(path.join(workdir, '.adoremix-installed'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ensureWorkdir(workdir) {
|
|
28
|
+
if (!fs.existsSync(workdir)) {
|
|
29
|
+
fse.ensureDirSync(workdir);
|
|
30
|
+
logger.ok(`创建工作目录 ${workdir}`);
|
|
31
|
+
} else {
|
|
32
|
+
logger.info(`工作目录已存在 ${workdir}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function copyNative(native, workdir, force) {
|
|
37
|
+
if (!native.exists()) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`原生二进制 ${native.binName} 未找到。\n` +
|
|
40
|
+
`路径:${native.bin}\n` +
|
|
41
|
+
`请先运行拆分脚本:node scripts/split-zip.js --only ${paths.PLATFORM_KEY.replace('_', '-')}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const marker = path.join(workdir, '.adoremix-installed');
|
|
45
|
+
if (fs.existsSync(marker) && !force) {
|
|
46
|
+
logger.warn(`工作目录已初始化(存在 ${marker})。使用 --force 覆盖。`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
logger.info(`复制 native 资源 ${native.root} -> ${workdir}`);
|
|
50
|
+
fse.copySync(native.root, workdir, {
|
|
51
|
+
filter: (s) => {
|
|
52
|
+
const base = path.basename(s);
|
|
53
|
+
return base !== 'README.md' || path.dirname(s) !== native.root;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
fs.writeFileSync(marker, JSON.stringify({
|
|
57
|
+
version: require('../package.json').version,
|
|
58
|
+
platform: native.platform,
|
|
59
|
+
arch: native.arch,
|
|
60
|
+
binName: native.binName,
|
|
61
|
+
installedAt: new Date().toISOString()
|
|
62
|
+
}, null, 2), 'utf8');
|
|
63
|
+
logger.ok('资源复制完成');
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function chmodBinaries(workdir, native) {
|
|
68
|
+
if (process.platform === 'win32') return;
|
|
69
|
+
for (const rel of [native.binName, 'lame', 'xiaoboshu.py']) {
|
|
70
|
+
const p = path.join(workdir, rel);
|
|
71
|
+
if (fs.existsSync(p)) {
|
|
72
|
+
fs.chmodSync(p, 0o755);
|
|
73
|
+
logger.log(` chmod 0755 ${rel}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ensureEmptyDirs(workdir) {
|
|
79
|
+
for (const d of EMPTY_DIRS) {
|
|
80
|
+
const p = path.join(workdir, d);
|
|
81
|
+
fse.ensureDirSync(p);
|
|
82
|
+
}
|
|
83
|
+
logger.ok('创建空目录:var/ logs/ dmp/ tty/ temp/');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeHelperPackageJson(workdir) {
|
|
87
|
+
const pkgPath = path.join(workdir, 'package.json');
|
|
88
|
+
const existing = fs.existsSync(pkgPath) ? JSON.parse(fs.readFileSync(pkgPath, 'utf8')) : {};
|
|
89
|
+
existing.name = existing.name || 'adoremix-workdir';
|
|
90
|
+
existing.version = existing.version || '1.0.0';
|
|
91
|
+
existing.private = true;
|
|
92
|
+
existing.dependencies = Object.assign({}, NODE_HELPER_DEPS, existing.dependencies || {});
|
|
93
|
+
fs.writeFileSync(pkgPath, JSON.stringify(existing, null, 2), 'utf8');
|
|
94
|
+
logger.ok('写入 package.json(tts.js/find.js/findsc.js 协作依赖)');
|
|
95
|
+
return pkgPath;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function installHelperDeps(workdir, skipNpmInstall) {
|
|
99
|
+
if (skipNpmInstall) {
|
|
100
|
+
logger.warn('--skip-npm-install 跳过协作依赖安装');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
logger.info('在工作目录安装 Node 协作依赖(axios/ws/crypto-js/mysql 等)...');
|
|
104
|
+
try {
|
|
105
|
+
execSync('npm install --production --no-audit --no-fund', {
|
|
106
|
+
cwd: workdir,
|
|
107
|
+
stdio: 'inherit'
|
|
108
|
+
});
|
|
109
|
+
logger.ok('协作依赖安装完成');
|
|
110
|
+
} catch (e) {
|
|
111
|
+
logger.error('npm install 失败,请手动到工作目录执行 npm install');
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function runInstall(opts) {
|
|
117
|
+
opts = opts || {};
|
|
118
|
+
const workdir = opts.workdir || paths.defaultWorkdir();
|
|
119
|
+
let native;
|
|
120
|
+
try {
|
|
121
|
+
native = opts.native || paths.loadNative();
|
|
122
|
+
} catch (e) {
|
|
123
|
+
logger.error(e.message);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
if (native.notice) {
|
|
127
|
+
logger.warn(native.notice);
|
|
128
|
+
}
|
|
129
|
+
logger.log(`平台 ${native.platform}-${native.arch}`);
|
|
130
|
+
logger.log(`工作目录 ${workdir}`);
|
|
131
|
+
logger.log(`主二进制 ${native.binName}`);
|
|
132
|
+
logger.log('');
|
|
133
|
+
|
|
134
|
+
ensureWorkdir(workdir);
|
|
135
|
+
let copied;
|
|
136
|
+
try {
|
|
137
|
+
copied = copyNative(native, workdir, opts.force);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
logger.error(e.message);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
if (!copied && !opts.force) return 1;
|
|
143
|
+
|
|
144
|
+
chmodBinaries(workdir, native);
|
|
145
|
+
ensureEmptyDirs(workdir);
|
|
146
|
+
writeHelperPackageJson(workdir);
|
|
147
|
+
try {
|
|
148
|
+
installHelperDeps(workdir, opts.skipNpmInstall);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const config = require('./config');
|
|
154
|
+
const cfgOk = await config.ensureConfig(workdir, { interactive: opts.interactive !== false, force: opts.force });
|
|
155
|
+
if (!cfgOk) {
|
|
156
|
+
logger.warn('配置未生成。请稍后运行 adoremix config init');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
logger.log('');
|
|
160
|
+
logger.ok('安装完成。');
|
|
161
|
+
logger.log(' 启动(前台) adoremix start');
|
|
162
|
+
logger.log(' 开机自启 adoremix service install');
|
|
163
|
+
logger.log(' 查看状态 adoremix status');
|
|
164
|
+
logger.log(' 查看日志 adoremix logs --follow');
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = {
|
|
169
|
+
runInstall,
|
|
170
|
+
EMPTY_DIRS,
|
|
171
|
+
NODE_HELPER_DEPS,
|
|
172
|
+
isWorkdirInitialized
|
|
173
|
+
};
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const isatty = process.stdout.isTTY;
|
|
4
|
+
|
|
5
|
+
function ts() {
|
|
6
|
+
return new Date().toISOString();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function fmt(level, args) {
|
|
10
|
+
return [`${ts()} [${level}]`].concat(Array.from(args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
log(...args) {
|
|
15
|
+
console.log(...args);
|
|
16
|
+
},
|
|
17
|
+
info(...args) {
|
|
18
|
+
console.log(...(isatty ? ['\x1b[36m[info]\x1b[0m'] : ['[info]']).concat(args));
|
|
19
|
+
},
|
|
20
|
+
warn(...args) {
|
|
21
|
+
console.warn(...(isatty ? ['\x1b[33m[warn]\x1b[0m'] : ['[warn]']).concat(args));
|
|
22
|
+
},
|
|
23
|
+
error(...args) {
|
|
24
|
+
console.error(...(isatty ? ['\x1b[31m[err ]\x1b[0m'] : ['[err ]']).concat(args));
|
|
25
|
+
},
|
|
26
|
+
ok(...args) {
|
|
27
|
+
console.log(...(isatty ? ['\x1b[32m[ok ]\x1b[0m'] : ['[ok ]']).concat(args));
|
|
28
|
+
}
|
|
29
|
+
};
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const PLATFORM_KEY = `${process.platform}_${process.arch}`;
|
|
8
|
+
const PLATFORM_PKG = {
|
|
9
|
+
'win32_x64': '@oxiaom/adoremix-win32-x64',
|
|
10
|
+
'linux_x64': '@oxiaom/adoremix-linux-x64',
|
|
11
|
+
'linux_arm64': '@oxiaom/adoremix-linux-arm64',
|
|
12
|
+
'linux_arm': '@oxiaom/adoremix-linux-arm'
|
|
13
|
+
}[PLATFORM_KEY];
|
|
14
|
+
|
|
15
|
+
let _native = null;
|
|
16
|
+
let _loadError = null;
|
|
17
|
+
|
|
18
|
+
function loadNative() {
|
|
19
|
+
if (_native) return _native;
|
|
20
|
+
if (_loadError) throw _loadError;
|
|
21
|
+
if (!PLATFORM_PKG) {
|
|
22
|
+
_loadError = new Error(`不支持的平台:${PLATFORM_KEY}。当前支持 win32-x64 / linux-x64 / linux-arm64 / linux-arm。`);
|
|
23
|
+
throw _loadError;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
_native = require(PLATFORM_PKG);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
_loadError = new Error(
|
|
29
|
+
`未安装平台子包 ${PLATFORM_PKG}。可能原因:\n` +
|
|
30
|
+
` - npm install 时被 --no-optional 跳过\n` +
|
|
31
|
+
` - 子包尚未发布到 npm registry\n` +
|
|
32
|
+
`原始错误:${e.message}`
|
|
33
|
+
);
|
|
34
|
+
throw _loadError;
|
|
35
|
+
}
|
|
36
|
+
return _native;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultWorkdir() {
|
|
40
|
+
if (process.platform === 'win32') {
|
|
41
|
+
const base = process.env.PROGRAMDATA || 'C:\\ProgramData';
|
|
42
|
+
return path.join(base, 'adoremix');
|
|
43
|
+
}
|
|
44
|
+
if (process.getuid && process.getuid() === 0) {
|
|
45
|
+
return '/opt/adoremix';
|
|
46
|
+
}
|
|
47
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
48
|
+
return xdg ? path.join(xdg, 'adoremix') : path.join(os.homedir(), '.local', 'share', 'adoremix');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function workdirPaths(workdir) {
|
|
52
|
+
return {
|
|
53
|
+
root: workdir,
|
|
54
|
+
config: path.join(workdir, 'config.ini'),
|
|
55
|
+
bin: null,
|
|
56
|
+
lame: null,
|
|
57
|
+
pidfile: path.join(workdir, 'var', 'app.pid'),
|
|
58
|
+
logfile: path.join(workdir, 'var', 'app.log'),
|
|
59
|
+
svcLog: path.join(workdir, 'logs', 'svc.log'),
|
|
60
|
+
svcErr: path.join(workdir, 'logs', 'svc.err'),
|
|
61
|
+
varDir: path.join(workdir, 'var'),
|
|
62
|
+
logsDir: path.join(workdir, 'logs'),
|
|
63
|
+
dmpDir: path.join(workdir, 'dmp'),
|
|
64
|
+
ttyDir: path.join(workdir, 'tty'),
|
|
65
|
+
tempDir: path.join(workdir, 'temp'),
|
|
66
|
+
etcDir: path.join(workdir, 'etc'),
|
|
67
|
+
confDir: path.join(workdir, 'conf'),
|
|
68
|
+
htmlDir: path.join(workdir, 'html'),
|
|
69
|
+
nodeModulesDir: path.join(workdir, 'node_modules')
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cliRoot() {
|
|
74
|
+
return path.join(__dirname, '..');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function nodeExecutable() {
|
|
78
|
+
return process.execPath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cliBin() {
|
|
82
|
+
return path.join(cliRoot(), 'bin', 'adoremix.js');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
PLATFORM_KEY,
|
|
87
|
+
PLATFORM_PKG,
|
|
88
|
+
loadNative,
|
|
89
|
+
defaultWorkdir,
|
|
90
|
+
workdirPaths,
|
|
91
|
+
cliRoot,
|
|
92
|
+
cliBin,
|
|
93
|
+
nodeExecutable
|
|
94
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const pidusage = require('pidusage');
|
|
6
|
+
|
|
7
|
+
function readPid(pidfile) {
|
|
8
|
+
if (!fs.existsSync(pidfile)) return null;
|
|
9
|
+
const txt = fs.readFileSync(pidfile, 'utf8').trim();
|
|
10
|
+
if (!txt) return null;
|
|
11
|
+
const n = parseInt(txt, 10);
|
|
12
|
+
return Number.isFinite(n) ? n : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writePid(pidfile, pid) {
|
|
16
|
+
fs.mkdirSync(path.dirname(pidfile), { recursive: true });
|
|
17
|
+
fs.writeFileSync(pidfile, String(pid), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clearPid(pidfile) {
|
|
21
|
+
if (fs.existsSync(pidfile)) fs.unlinkSync(pidfile);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRunning(pid) {
|
|
25
|
+
if (!pid) return false;
|
|
26
|
+
try {
|
|
27
|
+
process.kill(pid, 0);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return e.code === 'EPERM';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function stat(pid) {
|
|
35
|
+
if (!pid) return null;
|
|
36
|
+
try {
|
|
37
|
+
const s = await pidusage(pid);
|
|
38
|
+
return s;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function killPid(pid, timeout) {
|
|
45
|
+
timeout = timeout || 8000;
|
|
46
|
+
if (!isRunning(pid)) return true;
|
|
47
|
+
const sig = process.platform === 'win32' ? 'SIGTERM' : 'SIGTERM';
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, sig);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
if (e.code !== 'ESRCH') throw e;
|
|
52
|
+
}
|
|
53
|
+
const deadline = Date.now() + timeout;
|
|
54
|
+
while (Date.now() < deadline) {
|
|
55
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
56
|
+
if (!isRunning(pid)) return true;
|
|
57
|
+
}
|
|
58
|
+
if (process.platform !== 'win32') {
|
|
59
|
+
try { process.kill(pid, 'SIGKILL'); } catch (e) {}
|
|
60
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
61
|
+
}
|
|
62
|
+
return !isRunning(pid);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { readPid, writePid, clearPid, isRunning, stat, killPid };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const logger = require('../logger');
|
|
4
|
+
|
|
5
|
+
function installGlobalHandlers(context) {
|
|
6
|
+
const forward = (sig) => {
|
|
7
|
+
if (context && context.child && !context.child.killed) {
|
|
8
|
+
try { process.kill(context.child.pid, sig); } catch (e) {}
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
process.on('SIGINT', () => { logger.info('SIGINT 收到,转发'); forward('SIGINT'); });
|
|
12
|
+
process.on('SIGTERM', () => { logger.info('SIGTERM 收到,转发'); forward('SIGTERM'); });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { installGlobalHandlers };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const pidMgr = require('./pid');
|
|
8
|
+
const paths = require('../paths');
|
|
9
|
+
const logger = require('../logger');
|
|
10
|
+
|
|
11
|
+
function ensureLogFiles(workdirPaths) {
|
|
12
|
+
fs.mkdirSync(workdirPaths.varDir, { recursive: true });
|
|
13
|
+
fs.mkdirSync(workdirPaths.logsDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function startForeground(opts) {
|
|
17
|
+
const native = opts.native;
|
|
18
|
+
const workdir = opts.workdir;
|
|
19
|
+
const configPath = opts.configPath;
|
|
20
|
+
const wp = paths.workdirPaths(workdir);
|
|
21
|
+
|
|
22
|
+
ensureLogFiles(wp);
|
|
23
|
+
if (!fs.existsSync(configPath)) {
|
|
24
|
+
throw new Error(`配置不存在 ${configPath},请先运行 adoremix install`);
|
|
25
|
+
}
|
|
26
|
+
if (!fs.existsSync(native.bin)) {
|
|
27
|
+
throw new Error(`原生二进制不存在 ${native.bin}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const args = [path.basename(configPath)];
|
|
31
|
+
logger.info(`spawn ${native.binName} ${args.join(' ')} (cwd=${workdir})`);
|
|
32
|
+
const child = spawn(native.bin, args, {
|
|
33
|
+
cwd: workdir,
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
windowsHide: false
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
child.stdout.on('data', (d) => process.stdout.write(d));
|
|
39
|
+
child.stderr.on('data', (d) => process.stderr.write(d));
|
|
40
|
+
|
|
41
|
+
pidMgr.writePid(wp.pidfile, child.pid);
|
|
42
|
+
logger.ok(`已启动 PID=${child.pid}(前台模式,Ctrl+C 退出)`);
|
|
43
|
+
|
|
44
|
+
const forward = (sig) => {
|
|
45
|
+
if (!child.killed) {
|
|
46
|
+
try { process.kill(child.pid, sig); } catch (e) {}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
process.on('SIGINT', () => forward('SIGINT'));
|
|
50
|
+
process.on('SIGTERM', () => forward('SIGTERM'));
|
|
51
|
+
|
|
52
|
+
child.on('exit', (code, signal) => {
|
|
53
|
+
pidMgr.clearPid(wp.pidfile);
|
|
54
|
+
logger.log(`子进程退出 code=${code} signal=${signal}`);
|
|
55
|
+
process.exit(code == null ? 0 : code);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return child;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function startDaemon(opts) {
|
|
62
|
+
const native = opts.native;
|
|
63
|
+
const workdir = opts.workdir;
|
|
64
|
+
const configPath = opts.configPath;
|
|
65
|
+
const wp = paths.workdirPaths(workdir);
|
|
66
|
+
|
|
67
|
+
ensureLogFiles(wp);
|
|
68
|
+
if (!fs.existsSync(configPath)) {
|
|
69
|
+
throw new Error(`配置不存在 ${configPath},请先运行 adoremix install`);
|
|
70
|
+
}
|
|
71
|
+
if (!fs.existsSync(native.bin)) {
|
|
72
|
+
throw new Error(`原生二进制不存在 ${native.bin}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const running = pidMgr.readPid(wp.pidfile);
|
|
76
|
+
if (running && pidMgr.isRunning(running)) {
|
|
77
|
+
logger.warn(`已在运行 PID=${running}`);
|
|
78
|
+
return { alreadyRunning: true, pid: running };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const out = fs.openSync(wp.logfile, 'a');
|
|
82
|
+
const err = fs.openSync(wp.logfile, 'a');
|
|
83
|
+
const args = [path.basename(configPath)];
|
|
84
|
+
logger.info(`spawn ${native.binName} ${args.join(' ')} (cwd=${workdir}, daemon)`);
|
|
85
|
+
const child = spawn(native.bin, args, {
|
|
86
|
+
cwd: workdir,
|
|
87
|
+
stdio: ['ignore', out, err],
|
|
88
|
+
detached: true,
|
|
89
|
+
windowsHide: false
|
|
90
|
+
});
|
|
91
|
+
child.unref();
|
|
92
|
+
pidMgr.writePid(wp.pidfile, child.pid);
|
|
93
|
+
logger.ok(`已启动 PID=${child.pid}(后台模式,日志:${path.relative(workdir, wp.logfile)}`);
|
|
94
|
+
return { pid: child.pid };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function stop(opts) {
|
|
98
|
+
const workdir = opts.workdir;
|
|
99
|
+
const wp = paths.workdirPaths(workdir);
|
|
100
|
+
const pid = pidMgr.readPid(wp.pidfile);
|
|
101
|
+
if (!pid) {
|
|
102
|
+
logger.warn(`未找到 PID 文件 ${wp.pidfile}`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
if (!pidMgr.isRunning(pid)) {
|
|
106
|
+
logger.warn(`PID=${pid} 已不在运行,清理 pidfile`);
|
|
107
|
+
pidMgr.clearPid(wp.pidfile);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
logger.info(`停止 PID=${pid} ...`);
|
|
111
|
+
const ok = await pidMgr.killPid(pid, opts.timeout || 10000);
|
|
112
|
+
if (ok) {
|
|
113
|
+
pidMgr.clearPid(wp.pidfile);
|
|
114
|
+
logger.ok('已停止');
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
logger.error(`无法停止 PID=${pid},请用系统工具手动结束`);
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function status(opts) {
|
|
122
|
+
const workdir = opts.workdir;
|
|
123
|
+
const wp = paths.workdirPaths(workdir);
|
|
124
|
+
const pid = pidMgr.readPid(wp.pidfile);
|
|
125
|
+
if (!pid) {
|
|
126
|
+
logger.log('状态:未运行');
|
|
127
|
+
return 2;
|
|
128
|
+
}
|
|
129
|
+
if (!pidMgr.isRunning(pid)) {
|
|
130
|
+
logger.log(`状态:未运行(残留 pidfile,PID=${pid})`);
|
|
131
|
+
pidMgr.clearPid(wp.pidfile);
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
const s = await pidMgr.stat(pid);
|
|
135
|
+
const cpu = s ? s.cpu.toFixed(1) + '%' : '?';
|
|
136
|
+
const mem = s ? Math.round(s.memory / 1024 / 1024) + ' MB' : '?';
|
|
137
|
+
logger.log(`状态:运行中 PID=${pid} CPU=${cpu} MEM=${mem}`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function restart(opts) {
|
|
142
|
+
await stop(opts);
|
|
143
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
144
|
+
return opts.daemon ? startDaemon(opts) : startForeground(opts);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
startForeground,
|
|
149
|
+
startDaemon,
|
|
150
|
+
stop,
|
|
151
|
+
status,
|
|
152
|
+
restart
|
|
153
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const logger = require('../logger');
|
|
4
|
+
|
|
5
|
+
function pick() {
|
|
6
|
+
if (process.platform === 'win32') return require('./windows');
|
|
7
|
+
if (process.platform === 'linux') return require('./linux');
|
|
8
|
+
throw new Error(`service 子命令暂不支持平台 ${process.platform}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
install(opts) { return pick().install(opts); },
|
|
13
|
+
uninstall(opts) { return pick().uninstall(opts); },
|
|
14
|
+
status() { return pick().status(); }
|
|
15
|
+
};
|