altd 0.0.4 → 0.0.6
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 +20 -1
- package/dist/altd.js +122 -123
- package/dist/index.js +20 -4
- package/index.js +20 -4
- package/package.json +1 -1
- package/src/altd.js +122 -123
package/README.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
# altd
|
|
4
4
|
Web server access log tail dispatcher for running whitelisted commands based on GET requests.
|
|
5
5
|
|
|
6
|
+
This tool runs the command names you whitelist exactly as provided. There is no PATH
|
|
7
|
+
resolution, argument validation, rate limiting, or output limiting built in. Treat the
|
|
8
|
+
access log source as trusted input.
|
|
9
|
+
|
|
6
10
|
## Requirements
|
|
7
11
|
|
|
8
12
|
- Node.js 18 or later
|
|
@@ -29,7 +33,7 @@ altd <access_log.file> -w [commands]
|
|
|
29
33
|
|
|
30
34
|
- `-V, --version` output the version number
|
|
31
35
|
- `-h, --help` output usage information
|
|
32
|
-
- `-w, --whitelist <commands>` comma-separated list of allowed commands
|
|
36
|
+
- `-w, --whitelist <commands>` comma-separated list of allowed commands (executed directly)
|
|
33
37
|
|
|
34
38
|
## Example
|
|
35
39
|
|
|
@@ -37,6 +41,21 @@ altd <access_log.file> -w [commands]
|
|
|
37
41
|
altd /var/log/nginx/access_log -w ls,hostname
|
|
38
42
|
```
|
|
39
43
|
|
|
44
|
+
Log lines are expected to include a request line like:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
GET /hostname HTTP/1.1
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This would execute `hostname` with no arguments. Additional path segments are passed as
|
|
51
|
+
arguments, for example:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
GET /ls/-la HTTP/1.1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Would execute: `ls -la`.
|
|
58
|
+
|
|
40
59
|
## Contributing
|
|
41
60
|
|
|
42
61
|
Bug reports and pull requests are welcome on GitHub at https://github.com/freddiefujiwara/altd
|
package/dist/altd.js
CHANGED
|
@@ -1,132 +1,131 @@
|
|
|
1
|
-
import {spawn} from
|
|
2
|
-
import Tail from
|
|
3
|
-
|
|
4
|
-
** main class of AccessLogTailDispatcher
|
|
5
|
-
*/
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import Tail from "nodejs-tail";
|
|
3
|
+
|
|
6
4
|
export default class AccessLogTailDispatcher {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.spawn = undefined;
|
|
16
|
-
this.tail = undefined;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Get the path from 'GET /path/to/dir HTTP'
|
|
20
|
-
* @param {string} line access_log
|
|
21
|
-
* @return {string} /path/to/dir
|
|
22
|
-
*/
|
|
23
|
-
path(line) {
|
|
24
|
-
if (!(typeof line === 'string')) {
|
|
25
|
-
return '';
|
|
26
|
-
}
|
|
27
|
-
let match = line.match(
|
|
28
|
-
/GET\s((\/[a-z0-9-._~%!$&'()*+,;=:@?]+)+\/?)\sHTTP/i);
|
|
29
|
-
if (null !== match && match.length > 2) {
|
|
30
|
-
return match[1];
|
|
31
|
-
}
|
|
32
|
-
return '';
|
|
33
|
-
}
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} file access_log
|
|
7
|
+
* @param {object} commandRegistry { [commandName]: { execPath: string, buildArgs: (rawArgs:string[])=>string[] } }
|
|
8
|
+
* @param {object} [opts]
|
|
9
|
+
*/
|
|
10
|
+
constructor(file, commandRegistry, opts = {}) {
|
|
11
|
+
this.file = file;
|
|
12
|
+
this.registry = commandRegistry;
|
|
34
13
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
let commands = path.split(/\//).map(function(element, index, array) {
|
|
45
|
-
let ret = "";
|
|
46
|
-
try{
|
|
47
|
-
ret = decodeURIComponent(element);
|
|
48
|
-
}catch(e){
|
|
49
|
-
console.error(e);
|
|
50
|
-
}
|
|
51
|
-
return ret;
|
|
52
|
-
});
|
|
53
|
-
commands.shift();
|
|
54
|
-
return commands;
|
|
55
|
-
}
|
|
14
|
+
this.spawnImpl = opts.spawnImpl ?? nodeSpawn;
|
|
15
|
+
this.tail = opts.tail
|
|
16
|
+
?? new Tail(file, {
|
|
17
|
+
alwaysStat: true,
|
|
18
|
+
ignoreInitial: true,
|
|
19
|
+
persistent: true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
56
22
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
!this.isArray(whitelist) ||
|
|
66
|
-
commandWithArgs.length == 0 ||
|
|
67
|
-
whitelist.indexOf(commandWithArgs[0]) == -1
|
|
68
|
-
) {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
return commandWithArgs;
|
|
72
|
-
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract request path from a typical access log line.
|
|
25
|
+
* More robust: parse "METHOD <url> HTTP/..."
|
|
26
|
+
* @param {string} line
|
|
27
|
+
* @returns {string} pathname like "/a/b"
|
|
28
|
+
*/
|
|
29
|
+
extractPath(line) {
|
|
30
|
+
if (typeof line !== "string") return "";
|
|
73
31
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (commandWithArgs.length == 0) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
let command = commandWithArgs.shift();
|
|
83
|
-
let proc = this.spawn(command, commandWithArgs);
|
|
84
|
-
proc.on('error', (err) => {
|
|
85
|
-
console.error(err);
|
|
86
|
-
});
|
|
87
|
-
proc.stdout.on('data', (data) => {
|
|
88
|
-
process.stdout.write(data.toString());
|
|
89
|
-
});
|
|
90
|
-
}
|
|
32
|
+
// Find something like: GET /foo/bar HTTP/1.1
|
|
33
|
+
const m = line.match(
|
|
34
|
+
/\b(GET|POST|PUT|DELETE|HEAD|OPTIONS)\s+(\S+)\s+HTTP\/\d(?:\.\d)?\b/i,
|
|
35
|
+
);
|
|
36
|
+
if (!m) return "";
|
|
91
37
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return
|
|
38
|
+
const rawTarget = m[2];
|
|
39
|
+
|
|
40
|
+
if (rawTarget.startsWith("http://") || rawTarget.startsWith("https://")) {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(rawTarget).pathname || "";
|
|
43
|
+
} catch {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
99
46
|
}
|
|
100
47
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.commandWithArgs(
|
|
124
|
-
this.path(line)),
|
|
125
|
-
this.whitelist));
|
|
126
|
-
});
|
|
127
|
-
this.tail.on('close', () => {
|
|
128
|
-
console.log('watching stopped');
|
|
129
|
-
});
|
|
130
|
-
this.tail.watch();
|
|
48
|
+
return rawTarget;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* "/cmd/a/b" -> ["cmd","a","b"] (safe decode, size limits)
|
|
53
|
+
* @param {string} pathname
|
|
54
|
+
* @returns {string[]}
|
|
55
|
+
*/
|
|
56
|
+
parseCommand(pathname) {
|
|
57
|
+
if (typeof pathname !== "string" || pathname === "") return [];
|
|
58
|
+
if (!pathname.startsWith("/")) return [];
|
|
59
|
+
|
|
60
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
61
|
+
if (parts.length === 0) return [];
|
|
62
|
+
|
|
63
|
+
const decoded = [];
|
|
64
|
+
for (const p of parts) {
|
|
65
|
+
try {
|
|
66
|
+
decoded.push(decodeURIComponent(p));
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
131
70
|
}
|
|
71
|
+
return decoded;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validate + build exec + args using registry
|
|
76
|
+
* @param {string[]} parsed ["cmd", ...rawArgs]
|
|
77
|
+
* @returns {{execPath:string,args:string[]}|null}
|
|
78
|
+
*/
|
|
79
|
+
resolveExecution(parsed) {
|
|
80
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return null;
|
|
81
|
+
|
|
82
|
+
const [cmd, ...rawArgs] = parsed;
|
|
83
|
+
|
|
84
|
+
const entry = this.registry[cmd];
|
|
85
|
+
if (!entry) return null;
|
|
86
|
+
|
|
87
|
+
const buildArgs = entry.buildArgs ?? ((args) => args);
|
|
88
|
+
const args = buildArgs(rawArgs);
|
|
89
|
+
if (!Array.isArray(args)) return null;
|
|
90
|
+
|
|
91
|
+
return { execPath: entry.execPath, args };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
spawnCommand(execPath, args) {
|
|
95
|
+
const proc = this.spawnImpl(execPath, args, {
|
|
96
|
+
shell: false,
|
|
97
|
+
windowsHide: true,
|
|
98
|
+
stdio: "inherit",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
proc.on("error", (err) => {
|
|
102
|
+
console.error("[spawn error]", err);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Start watching
|
|
108
|
+
*/
|
|
109
|
+
run() {
|
|
110
|
+
this.tail.on("line", (line) => {
|
|
111
|
+
const pathname = this.extractPath(line);
|
|
112
|
+
const parsed = this.parseCommand(pathname);
|
|
113
|
+
const exec = this.resolveExecution(parsed);
|
|
114
|
+
if (!exec) return;
|
|
115
|
+
|
|
116
|
+
this.spawnCommand(exec.execPath, exec.args);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
this.tail.on("close", () => {
|
|
120
|
+
console.log("watching stopped");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.tail.watch();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
stop() {
|
|
127
|
+
try {
|
|
128
|
+
this.tail.unwatch?.();
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
132
131
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { Command } from 'commander';
|
|
3
|
-
import pkg from '../package.json' assert { type: 'json' };
|
|
4
4
|
import AccessLogTailDispatcher from './altd.js';
|
|
5
5
|
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const pkg = require('../package.json');
|
|
8
|
+
|
|
6
9
|
const program = new Command();
|
|
7
10
|
|
|
11
|
+
const buildRegistry = (whitelist) => {
|
|
12
|
+
const registry = {};
|
|
13
|
+
for (const command of whitelist) {
|
|
14
|
+
registry[command] = {
|
|
15
|
+
execPath: command,
|
|
16
|
+
buildArgs: (rawArgs) => rawArgs,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return registry;
|
|
20
|
+
};
|
|
21
|
+
|
|
8
22
|
program
|
|
9
23
|
.name('altd')
|
|
10
24
|
.version(pkg.version)
|
|
@@ -13,17 +27,19 @@ program
|
|
|
13
27
|
.option(
|
|
14
28
|
'-w, --whitelist <commands>',
|
|
15
29
|
'Add commands to whitelist',
|
|
16
|
-
(commands) => commands.split(',')
|
|
30
|
+
(commands) => commands.split(',').map((command) => command.trim()).filter(Boolean)
|
|
17
31
|
)
|
|
18
32
|
.parse(process.argv);
|
|
19
33
|
|
|
20
34
|
const fileValue = program.args[0];
|
|
21
35
|
const { whitelist } = program.opts();
|
|
22
36
|
|
|
23
|
-
if (!fileValue || !whitelist) {
|
|
37
|
+
if (!fileValue || !whitelist || whitelist.length === 0) {
|
|
24
38
|
console.log('altd <file> -w <commands...>');
|
|
25
39
|
process.exit(1);
|
|
26
40
|
}
|
|
27
41
|
|
|
28
|
-
const
|
|
42
|
+
const registry = buildRegistry(whitelist);
|
|
43
|
+
|
|
44
|
+
const altd = new AccessLogTailDispatcher(fileValue, registry);
|
|
29
45
|
altd.run();
|
package/index.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { Command } from 'commander';
|
|
3
|
-
import pkg from './package.json' assert { type: 'json' };
|
|
4
4
|
import AccessLogTailDispatcher from './src/altd.js';
|
|
5
5
|
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const pkg = require('./package.json');
|
|
8
|
+
|
|
6
9
|
const program = new Command();
|
|
7
10
|
|
|
11
|
+
const buildRegistry = (whitelist) => {
|
|
12
|
+
const registry = {};
|
|
13
|
+
for (const command of whitelist) {
|
|
14
|
+
registry[command] = {
|
|
15
|
+
execPath: command,
|
|
16
|
+
buildArgs: (rawArgs) => rawArgs,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return registry;
|
|
20
|
+
};
|
|
21
|
+
|
|
8
22
|
program
|
|
9
23
|
.name('altd')
|
|
10
24
|
.version(pkg.version)
|
|
@@ -13,17 +27,19 @@ program
|
|
|
13
27
|
.option(
|
|
14
28
|
'-w, --whitelist <commands>',
|
|
15
29
|
'Add commands to whitelist',
|
|
16
|
-
(commands) => commands.split(',')
|
|
30
|
+
(commands) => commands.split(',').map((command) => command.trim()).filter(Boolean)
|
|
17
31
|
)
|
|
18
32
|
.parse(process.argv);
|
|
19
33
|
|
|
20
34
|
const fileValue = program.args[0];
|
|
21
35
|
const { whitelist } = program.opts();
|
|
22
36
|
|
|
23
|
-
if (!fileValue || !whitelist) {
|
|
37
|
+
if (!fileValue || !whitelist || whitelist.length === 0) {
|
|
24
38
|
console.log('altd <file> -w <commands...>');
|
|
25
39
|
process.exit(1);
|
|
26
40
|
}
|
|
27
41
|
|
|
28
|
-
const
|
|
42
|
+
const registry = buildRegistry(whitelist);
|
|
43
|
+
|
|
44
|
+
const altd = new AccessLogTailDispatcher(fileValue, registry);
|
|
29
45
|
altd.run();
|
package/package.json
CHANGED
package/src/altd.js
CHANGED
|
@@ -1,132 +1,131 @@
|
|
|
1
|
-
import {spawn} from
|
|
2
|
-
import Tail from
|
|
3
|
-
|
|
4
|
-
** main class of AccessLogTailDispatcher
|
|
5
|
-
*/
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import Tail from "nodejs-tail";
|
|
3
|
+
|
|
6
4
|
export default class AccessLogTailDispatcher {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.spawn = undefined;
|
|
16
|
-
this.tail = undefined;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Get the path from 'GET /path/to/dir HTTP'
|
|
20
|
-
* @param {string} line access_log
|
|
21
|
-
* @return {string} /path/to/dir
|
|
22
|
-
*/
|
|
23
|
-
path(line) {
|
|
24
|
-
if (!(typeof line === 'string')) {
|
|
25
|
-
return '';
|
|
26
|
-
}
|
|
27
|
-
let match = line.match(
|
|
28
|
-
/GET\s((\/[a-z0-9-._~%!$&'()*+,;=:@?]+)+\/?)\sHTTP/i);
|
|
29
|
-
if (null !== match && match.length > 2) {
|
|
30
|
-
return match[1];
|
|
31
|
-
}
|
|
32
|
-
return '';
|
|
33
|
-
}
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} file access_log
|
|
7
|
+
* @param {object} commandRegistry { [commandName]: { execPath: string, buildArgs: (rawArgs:string[])=>string[] } }
|
|
8
|
+
* @param {object} [opts]
|
|
9
|
+
*/
|
|
10
|
+
constructor(file, commandRegistry, opts = {}) {
|
|
11
|
+
this.file = file;
|
|
12
|
+
this.registry = commandRegistry;
|
|
34
13
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
let commands = path.split(/\//).map(function(element, index, array) {
|
|
45
|
-
let ret = "";
|
|
46
|
-
try{
|
|
47
|
-
ret = decodeURIComponent(element);
|
|
48
|
-
}catch(e){
|
|
49
|
-
console.error(e);
|
|
50
|
-
}
|
|
51
|
-
return ret;
|
|
52
|
-
});
|
|
53
|
-
commands.shift();
|
|
54
|
-
return commands;
|
|
55
|
-
}
|
|
14
|
+
this.spawnImpl = opts.spawnImpl ?? nodeSpawn;
|
|
15
|
+
this.tail = opts.tail
|
|
16
|
+
?? new Tail(file, {
|
|
17
|
+
alwaysStat: true,
|
|
18
|
+
ignoreInitial: true,
|
|
19
|
+
persistent: true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
56
22
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
!this.isArray(whitelist) ||
|
|
66
|
-
commandWithArgs.length == 0 ||
|
|
67
|
-
whitelist.indexOf(commandWithArgs[0]) == -1
|
|
68
|
-
) {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
return commandWithArgs;
|
|
72
|
-
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract request path from a typical access log line.
|
|
25
|
+
* More robust: parse "METHOD <url> HTTP/..."
|
|
26
|
+
* @param {string} line
|
|
27
|
+
* @returns {string} pathname like "/a/b"
|
|
28
|
+
*/
|
|
29
|
+
extractPath(line) {
|
|
30
|
+
if (typeof line !== "string") return "";
|
|
73
31
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (commandWithArgs.length == 0) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
let command = commandWithArgs.shift();
|
|
83
|
-
let proc = this.spawn(command, commandWithArgs);
|
|
84
|
-
proc.on('error', (err) => {
|
|
85
|
-
console.error(err);
|
|
86
|
-
});
|
|
87
|
-
proc.stdout.on('data', (data) => {
|
|
88
|
-
process.stdout.write(data.toString());
|
|
89
|
-
});
|
|
90
|
-
}
|
|
32
|
+
// Find something like: GET /foo/bar HTTP/1.1
|
|
33
|
+
const m = line.match(
|
|
34
|
+
/\b(GET|POST|PUT|DELETE|HEAD|OPTIONS)\s+(\S+)\s+HTTP\/\d(?:\.\d)?\b/i,
|
|
35
|
+
);
|
|
36
|
+
if (!m) return "";
|
|
91
37
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return
|
|
38
|
+
const rawTarget = m[2];
|
|
39
|
+
|
|
40
|
+
if (rawTarget.startsWith("http://") || rawTarget.startsWith("https://")) {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(rawTarget).pathname || "";
|
|
43
|
+
} catch {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
99
46
|
}
|
|
100
47
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.commandWithArgs(
|
|
124
|
-
this.path(line)),
|
|
125
|
-
this.whitelist));
|
|
126
|
-
});
|
|
127
|
-
this.tail.on('close', () => {
|
|
128
|
-
console.log('watching stopped');
|
|
129
|
-
});
|
|
130
|
-
this.tail.watch();
|
|
48
|
+
return rawTarget;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* "/cmd/a/b" -> ["cmd","a","b"] (safe decode, size limits)
|
|
53
|
+
* @param {string} pathname
|
|
54
|
+
* @returns {string[]}
|
|
55
|
+
*/
|
|
56
|
+
parseCommand(pathname) {
|
|
57
|
+
if (typeof pathname !== "string" || pathname === "") return [];
|
|
58
|
+
if (!pathname.startsWith("/")) return [];
|
|
59
|
+
|
|
60
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
61
|
+
if (parts.length === 0) return [];
|
|
62
|
+
|
|
63
|
+
const decoded = [];
|
|
64
|
+
for (const p of parts) {
|
|
65
|
+
try {
|
|
66
|
+
decoded.push(decodeURIComponent(p));
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
131
70
|
}
|
|
71
|
+
return decoded;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validate + build exec + args using registry
|
|
76
|
+
* @param {string[]} parsed ["cmd", ...rawArgs]
|
|
77
|
+
* @returns {{execPath:string,args:string[]}|null}
|
|
78
|
+
*/
|
|
79
|
+
resolveExecution(parsed) {
|
|
80
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return null;
|
|
81
|
+
|
|
82
|
+
const [cmd, ...rawArgs] = parsed;
|
|
83
|
+
|
|
84
|
+
const entry = this.registry[cmd];
|
|
85
|
+
if (!entry) return null;
|
|
86
|
+
|
|
87
|
+
const buildArgs = entry.buildArgs ?? ((args) => args);
|
|
88
|
+
const args = buildArgs(rawArgs);
|
|
89
|
+
if (!Array.isArray(args)) return null;
|
|
90
|
+
|
|
91
|
+
return { execPath: entry.execPath, args };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
spawnCommand(execPath, args) {
|
|
95
|
+
const proc = this.spawnImpl(execPath, args, {
|
|
96
|
+
shell: false,
|
|
97
|
+
windowsHide: true,
|
|
98
|
+
stdio: "inherit",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
proc.on("error", (err) => {
|
|
102
|
+
console.error("[spawn error]", err);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Start watching
|
|
108
|
+
*/
|
|
109
|
+
run() {
|
|
110
|
+
this.tail.on("line", (line) => {
|
|
111
|
+
const pathname = this.extractPath(line);
|
|
112
|
+
const parsed = this.parseCommand(pathname);
|
|
113
|
+
const exec = this.resolveExecution(parsed);
|
|
114
|
+
if (!exec) return;
|
|
115
|
+
|
|
116
|
+
this.spawnCommand(exec.execPath, exec.args);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
this.tail.on("close", () => {
|
|
120
|
+
console.log("watching stopped");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.tail.watch();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
stop() {
|
|
127
|
+
try {
|
|
128
|
+
this.tail.unwatch?.();
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
132
131
|
}
|