@adecore/service 0.48.3 → 0.49.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/dist/cmd-line.d.ts +2 -0
- package/dist/cmd-line.js +11 -2
- package/dist/definitions.d.ts +5 -0
- package/dist/definitions.js +56 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/manager.js +12 -2
- package/package.json +1 -1
- package/src/cmd-line.ts +13 -2
- package/src/definitions.ts +66 -12
- package/src/index.ts +1 -1
- package/src/manager.ts +15 -2
package/dist/cmd-line.d.ts
CHANGED
package/dist/cmd-line.js
CHANGED
|
@@ -3,9 +3,18 @@ const CMD_SPECIAL = /([()\][%!^"`<>&|;, *?])/g;
|
|
|
3
3
|
export function cmdEscaped(text) {
|
|
4
4
|
return text.replace(CMD_SPECIAL, '^$1');
|
|
5
5
|
}
|
|
6
|
+
/* The same on a line of a batch file, where cmd.exe reads `%` before any caret: only doubling keeps one. */
|
|
7
|
+
export function batchEscaped(text) {
|
|
8
|
+
return text.replace(/[()\][!^"`<>&|;, *?]/g, '^$&').replace(/%/g, '%%');
|
|
9
|
+
}
|
|
10
|
+
/* The script of a task, beside its definition. Without anything that would need quoting on its command line. */
|
|
11
|
+
export function scheduledTaskScriptName(label) {
|
|
12
|
+
return `${label.replace(/[^\w.-]/g, '_')}.cmd`;
|
|
13
|
+
}
|
|
6
14
|
/*
|
|
7
|
-
* What a manager finds the cmd.exe of a task by: `schtasks /End` ends that
|
|
8
|
-
* has to end the tree under it first. Its title is the label, which is the name
|
|
15
|
+
* What a manager finds the cmd.exe of a task from before it had a script by: `schtasks /End` ends that
|
|
16
|
+
* process alone, so a stop has to end the tree under it first. Its title is the label, which is the name
|
|
17
|
+
* of the task. A task on a script is found by the script's name on the cmd.exe line.
|
|
9
18
|
*/
|
|
10
19
|
export function scheduledTaskMarker(label) {
|
|
11
20
|
return `title ${cmdEscaped(label)} && `;
|
package/dist/definitions.d.ts
CHANGED
|
@@ -7,8 +7,13 @@ export interface ServiceSpec {
|
|
|
7
7
|
workingDirectory: string;
|
|
8
8
|
logFile: string;
|
|
9
9
|
user?: string;
|
|
10
|
+
scriptDirectory?: string;
|
|
10
11
|
}
|
|
11
12
|
export declare function launchAgentPlist(spec: ServiceSpec): string;
|
|
12
13
|
export declare function systemdUnit(spec: ServiceSpec): string;
|
|
13
14
|
export declare function scheduledTask(spec: ServiceSpec): string;
|
|
15
|
+
export declare function scheduledTaskScript(definition: string): {
|
|
16
|
+
path: string;
|
|
17
|
+
text: string;
|
|
18
|
+
} | null;
|
|
14
19
|
export declare function definitionRunsProgram(definition: string, program: string): boolean;
|
package/dist/definitions.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { cmdEscaped,
|
|
1
|
+
import { batchEscaped, cmdEscaped, scheduledTaskScriptName } from './cmd-line.js';
|
|
2
2
|
function xmlText(value) {
|
|
3
3
|
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
4
4
|
}
|
|
5
5
|
function xmlValue(text) {
|
|
6
|
-
return text
|
|
6
|
+
return text
|
|
7
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/&/g, '&');
|
|
7
11
|
}
|
|
8
12
|
function plistString(value, indent) {
|
|
9
13
|
return `${indent}<string>${xmlText(value)}</string>`;
|
|
@@ -64,21 +68,44 @@ function cmdArgument(arg) {
|
|
|
64
68
|
if (/[\r\n]/.test(arg)) {
|
|
65
69
|
throw new Error('A scheduled task cannot pass on a line break');
|
|
66
70
|
}
|
|
67
|
-
return
|
|
71
|
+
return batchEscaped(`"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, '$1$1')}"`);
|
|
68
72
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
/* In quotes of its own, or the C runtime of the program splits its own path at a space, whatever the carets told cmd.exe. */
|
|
74
|
+
function batchProgram(program) {
|
|
75
|
+
return batchEscaped(`"${program}"`);
|
|
76
|
+
}
|
|
77
|
+
/* The code page first, or cmd.exe reads the UTF-8 of a path outside the system's code page as something else. */
|
|
78
|
+
function taskScript(spec) {
|
|
79
|
+
const variables = Object.entries(spec.environment).map(([name, value]) => `set ${batchEscaped(`"${name}=${value}"`)}`);
|
|
80
|
+
const program = [batchProgram(spec.program), ...spec.args.map(cmdArgument)].join(' ');
|
|
81
|
+
const lines = [
|
|
82
|
+
'@echo off',
|
|
83
|
+
'chcp 65001 >nul',
|
|
84
|
+
`cd /d ${batchEscaped(spec.workingDirectory)} || exit /b 1`,
|
|
85
|
+
...variables,
|
|
86
|
+
`${program} >>${batchEscaped(spec.logFile)} 2>&1`
|
|
87
|
+
];
|
|
88
|
+
if (lines.some((line) => /[\r\n]/.test(line))) {
|
|
89
|
+
throw new Error('A scheduled task cannot pass on a line break');
|
|
90
|
+
}
|
|
91
|
+
return `${lines.join('\r\n')}\r\n`;
|
|
73
92
|
}
|
|
74
93
|
/*
|
|
75
94
|
* Started at the person's logon without a stored password, and run again every minute so it comes back
|
|
76
|
-
* after a crash. RestartOnFailure would also start a task a stop ended.
|
|
95
|
+
* after a crash. RestartOnFailure would also start a task a stop ended.
|
|
96
|
+
*
|
|
97
|
+
* A task with an interactive token gives a console program a window, and closing that ends it. Only a
|
|
98
|
+
* headless conhost.exe shows none, but it joins its arguments again and loses every inner quote. So
|
|
99
|
+
* what needs quoting moves into a script, which the task starts from its own folder by a bare name.
|
|
100
|
+
* The definition carries the script in `Documentation`, and a manager writes it out on install.
|
|
77
101
|
*/
|
|
78
102
|
export function scheduledTask(spec) {
|
|
79
103
|
if (spec.user === undefined || spec.user === '') {
|
|
80
104
|
throw new Error('A scheduled task needs the user it starts for.');
|
|
81
105
|
}
|
|
106
|
+
if (spec.scriptDirectory === undefined || spec.scriptDirectory === '') {
|
|
107
|
+
throw new Error('A scheduled task needs the folder of its script.');
|
|
108
|
+
}
|
|
82
109
|
const user = xmlText(spec.user);
|
|
83
110
|
// No XML declaration: schtasks decodes the file itself and refuses one that names an encoding.
|
|
84
111
|
const lines = [
|
|
@@ -86,6 +113,7 @@ export function scheduledTask(spec) {
|
|
|
86
113
|
' <RegistrationInfo>',
|
|
87
114
|
` <Description>${xmlText(spec.description)}</Description>`,
|
|
88
115
|
` <URI>\\${xmlText(spec.label)}</URI>`,
|
|
116
|
+
` <Documentation>${xmlText(taskScript(spec))}</Documentation>`,
|
|
89
117
|
' </RegistrationInfo>',
|
|
90
118
|
' <Triggers>',
|
|
91
119
|
' <LogonTrigger>',
|
|
@@ -127,17 +155,33 @@ export function scheduledTask(spec) {
|
|
|
127
155
|
' </Settings>',
|
|
128
156
|
' <Actions Context="Author">',
|
|
129
157
|
' <Exec>',
|
|
130
|
-
' <Command>%SystemRoot%\\System32\\
|
|
131
|
-
` <Arguments
|
|
132
|
-
` <WorkingDirectory>${xmlText(spec.
|
|
158
|
+
' <Command>%SystemRoot%\\System32\\conhost.exe</Command>',
|
|
159
|
+
` <Arguments>--headless cmd.exe /d /c .\\${scheduledTaskScriptName(spec.label)}</Arguments>`,
|
|
160
|
+
` <WorkingDirectory>${xmlText(spec.scriptDirectory)}</WorkingDirectory>`,
|
|
133
161
|
' </Exec>',
|
|
134
162
|
' </Actions>',
|
|
135
163
|
'</Task>',
|
|
136
164
|
''
|
|
137
165
|
];
|
|
138
|
-
|
|
166
|
+
// ASCII with character references: schtasks reads a UTF-8 file as the system's code page, and refuses a BOM.
|
|
167
|
+
return lines.join('\r\n').replace(/[\u0080-\u{10ffff}]/gu, (character) => `&#${character.codePointAt(0)};`);
|
|
168
|
+
}
|
|
169
|
+
/* The script a definition starts, null for any other definition and for a task from before it had one. */
|
|
170
|
+
export function scheduledTaskScript(definition) {
|
|
171
|
+
const text = /<Documentation>([^<]*)<\/Documentation>/.exec(definition)?.[1];
|
|
172
|
+
const name = /<Arguments>--headless cmd\.exe \/d \/c \.\\([^<]+)<\/Arguments>/.exec(definition)?.[1];
|
|
173
|
+
const folder = /<WorkingDirectory>([^<]*)<\/WorkingDirectory>/.exec(definition)?.[1];
|
|
174
|
+
if (text === undefined || name === undefined || folder === undefined) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return { path: `${xmlValue(folder).replace(/\\$/, '')}\\${name}`, text: xmlValue(text) };
|
|
139
178
|
}
|
|
140
179
|
function scheduledTaskRunsProgram(definition, program) {
|
|
180
|
+
const script = scheduledTaskScript(definition);
|
|
181
|
+
if (script !== null) {
|
|
182
|
+
return (script.text.trimEnd().split('\r\n').at(-1) ?? '').startsWith(`${batchProgram(program)} `);
|
|
183
|
+
}
|
|
184
|
+
// A task from before the script: its one line ends in the program.
|
|
141
185
|
const exec = definition.match(/<Exec>[\s\S]*?<\/Exec>/g);
|
|
142
186
|
if (exec?.length !== 1) {
|
|
143
187
|
return false;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { definitionRunsProgram, launchAgentPlist, scheduledTask, systemdUnit, type ServiceSpec } from './definitions.js';
|
|
1
|
+
export { definitionRunsProgram, launchAgentPlist, scheduledTask, scheduledTaskScript, systemdUnit, type ServiceSpec } from './definitions.js';
|
|
2
2
|
export { launchdManager, systemdManager, taskSchedulerManager, type CommandResult, type CommandRunner, type LaunchdOptions, type ServiceFiles, type ServiceManager, type SystemdOptions, type TaskSchedulerOptions } from './manager.js';
|
|
3
3
|
export { platformServiceManager, serviceDefinition, type PlatformServiceOptions } from './platform.js';
|
|
4
4
|
export { diskFiles, runCommand } from './system.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { definitionRunsProgram, launchAgentPlist, scheduledTask, systemdUnit } from './definitions.js';
|
|
1
|
+
export { definitionRunsProgram, launchAgentPlist, scheduledTask, scheduledTaskScript, systemdUnit } from './definitions.js';
|
|
2
2
|
export { launchdManager, systemdManager, taskSchedulerManager } from './manager.js';
|
|
3
3
|
export { platformServiceManager, serviceDefinition } from './platform.js';
|
|
4
4
|
export { diskFiles, runCommand } from './system.js';
|
package/dist/manager.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { posix, win32 } from 'node:path';
|
|
2
|
-
import { scheduledTaskMarker } from './cmd-line.js';
|
|
2
|
+
import { scheduledTaskMarker, scheduledTaskScriptName } from './cmd-line.js';
|
|
3
|
+
import { scheduledTaskScript } from './definitions.js';
|
|
3
4
|
function expectSuccess(result, what) {
|
|
4
5
|
if (result.code !== 0) {
|
|
5
6
|
throw new Error(`${what} failed (${result.code}): ${(result.stderr || result.stdout).trim() || 'no output'}`);
|
|
@@ -134,9 +135,10 @@ export function taskSchedulerManager(options) {
|
|
|
134
135
|
// Before `/End`, which leaves what the task's cmd.exe started running without a parent to find it by.
|
|
135
136
|
const endTree = () => {
|
|
136
137
|
const marker = scheduledTaskMarker(name).replace(/'/g, "''");
|
|
138
|
+
const scriptName = scheduledTaskScriptName(name);
|
|
137
139
|
const script = [
|
|
138
140
|
'Get-CimInstance Win32_Process -Filter "Name=\'cmd.exe\'"',
|
|
139
|
-
`Where-Object { $_.CommandLine -and $_.CommandLine.Contains('${marker}') }`,
|
|
141
|
+
`Where-Object { $_.CommandLine -and ($_.CommandLine.Contains('${scriptName}') -or $_.CommandLine.Contains('${marker}')) }`,
|
|
140
142
|
'ForEach-Object { taskkill.exe /PID $_.ProcessId /T /F | Out-Null }'
|
|
141
143
|
].join(' | ');
|
|
142
144
|
options.run('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')]);
|
|
@@ -154,6 +156,10 @@ export function taskSchedulerManager(options) {
|
|
|
154
156
|
// cmd.exe appends to the log but makes no folder for it, and the task ends at once without one.
|
|
155
157
|
options.files.makeDirectory(win32.dirname(options.logFile));
|
|
156
158
|
const previous = writeDefinition(options.files, path, definition);
|
|
159
|
+
const script = scheduledTaskScript(definition);
|
|
160
|
+
if (script !== null && options.files.read(script.path) !== script.text) {
|
|
161
|
+
options.files.write(script.path, script.text);
|
|
162
|
+
}
|
|
157
163
|
if (previous !== definition || !exists()) {
|
|
158
164
|
expectSuccess(schtasks('/Create', '/TN', name, '/XML', path, '/F'), 'schtasks /Create');
|
|
159
165
|
}
|
|
@@ -177,6 +183,10 @@ export function taskSchedulerManager(options) {
|
|
|
177
183
|
},
|
|
178
184
|
uninstall() {
|
|
179
185
|
schtasks('/Delete', '/TN', name, '/F');
|
|
186
|
+
const script = scheduledTaskScript(options.files.read(path) ?? '');
|
|
187
|
+
if (script !== null) {
|
|
188
|
+
options.files.remove(script.path);
|
|
189
|
+
}
|
|
180
190
|
options.files.remove(path);
|
|
181
191
|
}
|
|
182
192
|
};
|
package/package.json
CHANGED
package/src/cmd-line.ts
CHANGED
|
@@ -5,9 +5,20 @@ export function cmdEscaped(text: string): string {
|
|
|
5
5
|
return text.replace(CMD_SPECIAL, '^$1');
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/* The same on a line of a batch file, where cmd.exe reads `%` before any caret: only doubling keeps one. */
|
|
9
|
+
export function batchEscaped(text: string): string {
|
|
10
|
+
return text.replace(/[()\][!^"`<>&|;, *?]/g, '^$&').replace(/%/g, '%%');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* The script of a task, beside its definition. Without anything that would need quoting on its command line. */
|
|
14
|
+
export function scheduledTaskScriptName(label: string): string {
|
|
15
|
+
return `${label.replace(/[^\w.-]/g, '_')}.cmd`;
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
/*
|
|
9
|
-
* What a manager finds the cmd.exe of a task by: `schtasks /End` ends that
|
|
10
|
-
* has to end the tree under it first. Its title is the label, which is the name
|
|
19
|
+
* What a manager finds the cmd.exe of a task from before it had a script by: `schtasks /End` ends that
|
|
20
|
+
* process alone, so a stop has to end the tree under it first. Its title is the label, which is the name
|
|
21
|
+
* of the task. A task on a script is found by the script's name on the cmd.exe line.
|
|
11
22
|
*/
|
|
12
23
|
export function scheduledTaskMarker(label: string): string {
|
|
13
24
|
return `title ${cmdEscaped(label)} && `;
|
package/src/definitions.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cmdEscaped,
|
|
1
|
+
import { batchEscaped, cmdEscaped, scheduledTaskScriptName } from './cmd-line.js';
|
|
2
2
|
|
|
3
3
|
export interface ServiceSpec {
|
|
4
4
|
label: string;
|
|
@@ -11,6 +11,8 @@ export interface ServiceSpec {
|
|
|
11
11
|
logFile: string;
|
|
12
12
|
/* The scheduled task only, which needs it: the account it starts for at logon, as `DOMAIN\\name`. */
|
|
13
13
|
user?: string;
|
|
14
|
+
/* The scheduled task only, which needs it: the folder of the script it runs. */
|
|
15
|
+
scriptDirectory?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function xmlText(value: string): string {
|
|
@@ -18,7 +20,11 @@ function xmlText(value: string): string {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function xmlValue(text: string): string {
|
|
21
|
-
return text
|
|
23
|
+
return text
|
|
24
|
+
.replace(/&#(\d+);/g, (_, code: string) => String.fromCodePoint(Number(code)))
|
|
25
|
+
.replace(/</g, '<')
|
|
26
|
+
.replace(/>/g, '>')
|
|
27
|
+
.replace(/&/g, '&');
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
function plistString(value: string, indent: string): string {
|
|
@@ -108,25 +114,51 @@ function cmdArgument(arg: string): string {
|
|
|
108
114
|
throw new Error('A scheduled task cannot pass on a line break');
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
return
|
|
117
|
+
return batchEscaped(`"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, '$1$1')}"`);
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
/* In quotes of its own, or the C runtime of the program splits its own path at a space, whatever the carets told cmd.exe. */
|
|
121
|
+
function batchProgram(program: string): string {
|
|
122
|
+
return batchEscaped(`"${program}"`);
|
|
123
|
+
}
|
|
117
124
|
|
|
118
|
-
|
|
125
|
+
/* The code page first, or cmd.exe reads the UTF-8 of a path outside the system's code page as something else. */
|
|
126
|
+
function taskScript(spec: ServiceSpec): string {
|
|
127
|
+
const variables = Object.entries(spec.environment).map(([name, value]) => `set ${batchEscaped(`"${name}=${value}"`)}`);
|
|
128
|
+
const program = [batchProgram(spec.program), ...spec.args.map(cmdArgument)].join(' ');
|
|
129
|
+
const lines = [
|
|
130
|
+
'@echo off',
|
|
131
|
+
'chcp 65001 >nul',
|
|
132
|
+
`cd /d ${batchEscaped(spec.workingDirectory)} || exit /b 1`,
|
|
133
|
+
...variables,
|
|
134
|
+
`${program} >>${batchEscaped(spec.logFile)} 2>&1`
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
if (lines.some((line) => /[\r\n]/.test(line))) {
|
|
138
|
+
throw new Error('A scheduled task cannot pass on a line break');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return `${lines.join('\r\n')}\r\n`;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
/*
|
|
122
145
|
* Started at the person's logon without a stored password, and run again every minute so it comes back
|
|
123
|
-
* after a crash. RestartOnFailure would also start a task a stop ended.
|
|
146
|
+
* after a crash. RestartOnFailure would also start a task a stop ended.
|
|
147
|
+
*
|
|
148
|
+
* A task with an interactive token gives a console program a window, and closing that ends it. Only a
|
|
149
|
+
* headless conhost.exe shows none, but it joins its arguments again and loses every inner quote. So
|
|
150
|
+
* what needs quoting moves into a script, which the task starts from its own folder by a bare name.
|
|
151
|
+
* The definition carries the script in `Documentation`, and a manager writes it out on install.
|
|
124
152
|
*/
|
|
125
153
|
export function scheduledTask(spec: ServiceSpec): string {
|
|
126
154
|
if (spec.user === undefined || spec.user === '') {
|
|
127
155
|
throw new Error('A scheduled task needs the user it starts for.');
|
|
128
156
|
}
|
|
129
157
|
|
|
158
|
+
if (spec.scriptDirectory === undefined || spec.scriptDirectory === '') {
|
|
159
|
+
throw new Error('A scheduled task needs the folder of its script.');
|
|
160
|
+
}
|
|
161
|
+
|
|
130
162
|
const user = xmlText(spec.user);
|
|
131
163
|
|
|
132
164
|
// No XML declaration: schtasks decodes the file itself and refuses one that names an encoding.
|
|
@@ -135,6 +167,7 @@ export function scheduledTask(spec: ServiceSpec): string {
|
|
|
135
167
|
' <RegistrationInfo>',
|
|
136
168
|
` <Description>${xmlText(spec.description)}</Description>`,
|
|
137
169
|
` <URI>\\${xmlText(spec.label)}</URI>`,
|
|
170
|
+
` <Documentation>${xmlText(taskScript(spec))}</Documentation>`,
|
|
138
171
|
' </RegistrationInfo>',
|
|
139
172
|
' <Triggers>',
|
|
140
173
|
' <LogonTrigger>',
|
|
@@ -176,19 +209,40 @@ export function scheduledTask(spec: ServiceSpec): string {
|
|
|
176
209
|
' </Settings>',
|
|
177
210
|
' <Actions Context="Author">',
|
|
178
211
|
' <Exec>',
|
|
179
|
-
' <Command>%SystemRoot%\\System32\\
|
|
180
|
-
` <Arguments
|
|
181
|
-
` <WorkingDirectory>${xmlText(spec.
|
|
212
|
+
' <Command>%SystemRoot%\\System32\\conhost.exe</Command>',
|
|
213
|
+
` <Arguments>--headless cmd.exe /d /c .\\${scheduledTaskScriptName(spec.label)}</Arguments>`,
|
|
214
|
+
` <WorkingDirectory>${xmlText(spec.scriptDirectory)}</WorkingDirectory>`,
|
|
182
215
|
' </Exec>',
|
|
183
216
|
' </Actions>',
|
|
184
217
|
'</Task>',
|
|
185
218
|
''
|
|
186
219
|
];
|
|
187
220
|
|
|
188
|
-
|
|
221
|
+
// ASCII with character references: schtasks reads a UTF-8 file as the system's code page, and refuses a BOM.
|
|
222
|
+
return lines.join('\r\n').replace(/[\u0080-\u{10ffff}]/gu, (character) => `&#${character.codePointAt(0)};`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* The script a definition starts, null for any other definition and for a task from before it had one. */
|
|
226
|
+
export function scheduledTaskScript(definition: string): { path: string; text: string } | null {
|
|
227
|
+
const text = /<Documentation>([^<]*)<\/Documentation>/.exec(definition)?.[1];
|
|
228
|
+
const name = /<Arguments>--headless cmd\.exe \/d \/c \.\\([^<]+)<\/Arguments>/.exec(definition)?.[1];
|
|
229
|
+
const folder = /<WorkingDirectory>([^<]*)<\/WorkingDirectory>/.exec(definition)?.[1];
|
|
230
|
+
|
|
231
|
+
if (text === undefined || name === undefined || folder === undefined) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { path: `${xmlValue(folder).replace(/\\$/, '')}\\${name}`, text: xmlValue(text) };
|
|
189
236
|
}
|
|
190
237
|
|
|
191
238
|
function scheduledTaskRunsProgram(definition: string, program: string): boolean {
|
|
239
|
+
const script = scheduledTaskScript(definition);
|
|
240
|
+
|
|
241
|
+
if (script !== null) {
|
|
242
|
+
return (script.text.trimEnd().split('\r\n').at(-1) ?? '').startsWith(`${batchProgram(program)} `);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// A task from before the script: its one line ends in the program.
|
|
192
246
|
const exec = definition.match(/<Exec>[\s\S]*?<\/Exec>/g);
|
|
193
247
|
|
|
194
248
|
if (exec?.length !== 1) {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { definitionRunsProgram, launchAgentPlist, scheduledTask, systemdUnit, type ServiceSpec } from './definitions.js';
|
|
1
|
+
export { definitionRunsProgram, launchAgentPlist, scheduledTask, scheduledTaskScript, systemdUnit, type ServiceSpec } from './definitions.js';
|
|
2
2
|
export {
|
|
3
3
|
launchdManager,
|
|
4
4
|
systemdManager,
|
package/src/manager.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { posix, win32 } from 'node:path';
|
|
2
|
-
import { scheduledTaskMarker } from './cmd-line.js';
|
|
2
|
+
import { scheduledTaskMarker, scheduledTaskScriptName } from './cmd-line.js';
|
|
3
|
+
import { scheduledTaskScript } from './definitions.js';
|
|
3
4
|
|
|
4
5
|
export interface CommandResult {
|
|
5
6
|
code: number;
|
|
@@ -231,10 +232,11 @@ export function taskSchedulerManager(options: TaskSchedulerOptions): ServiceMana
|
|
|
231
232
|
// Before `/End`, which leaves what the task's cmd.exe started running without a parent to find it by.
|
|
232
233
|
const endTree = (): void => {
|
|
233
234
|
const marker = scheduledTaskMarker(name).replace(/'/g, "''");
|
|
235
|
+
const scriptName = scheduledTaskScriptName(name);
|
|
234
236
|
|
|
235
237
|
const script = [
|
|
236
238
|
'Get-CimInstance Win32_Process -Filter "Name=\'cmd.exe\'"',
|
|
237
|
-
`Where-Object { $_.CommandLine -and $_.CommandLine.Contains('${marker}') }`,
|
|
239
|
+
`Where-Object { $_.CommandLine -and ($_.CommandLine.Contains('${scriptName}') -or $_.CommandLine.Contains('${marker}')) }`,
|
|
238
240
|
'ForEach-Object { taskkill.exe /PID $_.ProcessId /T /F | Out-Null }'
|
|
239
241
|
].join(' | ');
|
|
240
242
|
|
|
@@ -255,6 +257,11 @@ export function taskSchedulerManager(options: TaskSchedulerOptions): ServiceMana
|
|
|
255
257
|
// cmd.exe appends to the log but makes no folder for it, and the task ends at once without one.
|
|
256
258
|
options.files.makeDirectory(win32.dirname(options.logFile));
|
|
257
259
|
const previous = writeDefinition(options.files, path, definition);
|
|
260
|
+
const script = scheduledTaskScript(definition);
|
|
261
|
+
|
|
262
|
+
if (script !== null && options.files.read(script.path) !== script.text) {
|
|
263
|
+
options.files.write(script.path, script.text);
|
|
264
|
+
}
|
|
258
265
|
|
|
259
266
|
if (previous !== definition || !exists()) {
|
|
260
267
|
expectSuccess(schtasks('/Create', '/TN', name, '/XML', path, '/F'), 'schtasks /Create');
|
|
@@ -281,6 +288,12 @@ export function taskSchedulerManager(options: TaskSchedulerOptions): ServiceMana
|
|
|
281
288
|
},
|
|
282
289
|
uninstall() {
|
|
283
290
|
schtasks('/Delete', '/TN', name, '/F');
|
|
291
|
+
const script = scheduledTaskScript(options.files.read(path) ?? '');
|
|
292
|
+
|
|
293
|
+
if (script !== null) {
|
|
294
|
+
options.files.remove(script.path);
|
|
295
|
+
}
|
|
296
|
+
|
|
284
297
|
options.files.remove(path);
|
|
285
298
|
}
|
|
286
299
|
};
|