palmier 0.5.3 → 0.5.4
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/platform/windows.d.ts +1 -1
- package/dist/platform/windows.js +33 -29
- package/package.json +1 -1
- package/src/platform/windows.ts +29 -29
- package/test/windows-xml.test.ts +2 -0
|
@@ -20,7 +20,7 @@ export declare function buildTaskXml(tr: string, triggers: string[]): string;
|
|
|
20
20
|
export declare class WindowsPlatform implements PlatformService {
|
|
21
21
|
installDaemon(config: HostConfig): void;
|
|
22
22
|
restartDaemon(): Promise<void>;
|
|
23
|
-
/** Create or update the Task Scheduler entry for the daemon. */
|
|
23
|
+
/** Create or update the Task Scheduler entry for the daemon (requires elevation for S4U). */
|
|
24
24
|
private ensureDaemonTask;
|
|
25
25
|
/** Start the daemon via Task Scheduler (runs outside any session's job object). */
|
|
26
26
|
private startDaemonTask;
|
package/dist/platform/windows.js
CHANGED
|
@@ -6,7 +6,6 @@ import { getTaskDir, readTaskStatus } from "../task.js";
|
|
|
6
6
|
const TASK_PREFIX = "\\Palmier\\PalmierTask-";
|
|
7
7
|
const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
8
8
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
9
|
-
const DAEMON_VBS_FILE = path.join(CONFIG_DIR, "daemon.vbs");
|
|
10
9
|
const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
11
10
|
/**
|
|
12
11
|
* Convert a cron expression or "once" trigger to Task Scheduler XML trigger elements.
|
|
@@ -55,6 +54,12 @@ export function buildTaskXml(tr, triggers) {
|
|
|
55
54
|
return [
|
|
56
55
|
`<?xml version="1.0" encoding="UTF-16"?>`,
|
|
57
56
|
`<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">`,
|
|
57
|
+
` <Principals>`,
|
|
58
|
+
` <Principal>`,
|
|
59
|
+
` <LogonType>S4U</LogonType>`,
|
|
60
|
+
` <RunLevel>LeastPrivilege</RunLevel>`,
|
|
61
|
+
` </Principal>`,
|
|
62
|
+
` </Principals>`,
|
|
58
63
|
` <Settings>`,
|
|
59
64
|
` <MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>`,
|
|
60
65
|
` <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`,
|
|
@@ -77,22 +82,16 @@ function schtasksTaskName(taskId) {
|
|
|
77
82
|
export class WindowsPlatform {
|
|
78
83
|
installDaemon(config) {
|
|
79
84
|
const script = process.argv[1] || "palmier";
|
|
80
|
-
// Create the Task Scheduler entry for the daemon
|
|
85
|
+
// Create the Task Scheduler entry for the daemon (BootTrigger starts it at system boot)
|
|
81
86
|
this.ensureDaemonTask(script);
|
|
82
|
-
// Registry Run key
|
|
83
|
-
// so the daemon always runs outside any session's job object.
|
|
84
|
-
const regValue = `schtasks /run /tn "\\Palmier\\${DAEMON_TASK_NAME}"`;
|
|
87
|
+
// Remove old Registry Run key if upgrading
|
|
85
88
|
try {
|
|
86
89
|
execFileSync("reg", [
|
|
87
|
-
"
|
|
88
|
-
"/v", DAEMON_TASK_NAME, "/
|
|
90
|
+
"delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
|
|
91
|
+
"/v", DAEMON_TASK_NAME, "/f",
|
|
89
92
|
], { encoding: "utf-8", stdio: "pipe" });
|
|
90
|
-
console.log(`Registry Run key "${DAEMON_TASK_NAME}" installed (runs at logon).`);
|
|
91
|
-
}
|
|
92
|
-
catch (err) {
|
|
93
|
-
console.error(`Warning: failed to install registry run entry: ${err}`);
|
|
94
|
-
console.error("You may need to start palmier serve manually.");
|
|
95
93
|
}
|
|
94
|
+
catch { /* key may not exist */ }
|
|
96
95
|
// Start the daemon now
|
|
97
96
|
this.startDaemonTask();
|
|
98
97
|
console.log("\nHost initialization complete!");
|
|
@@ -133,19 +132,20 @@ export class WindowsPlatform {
|
|
|
133
132
|
}
|
|
134
133
|
this.startDaemonTask();
|
|
135
134
|
}
|
|
136
|
-
/** Create or update the Task Scheduler entry for the daemon. */
|
|
135
|
+
/** Create or update the Task Scheduler entry for the daemon (requires elevation for S4U). */
|
|
137
136
|
ensureDaemonTask(script) {
|
|
138
|
-
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
|
|
139
|
-
fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
|
|
140
|
-
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
141
137
|
const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
|
|
142
|
-
const tr = `"${
|
|
143
|
-
const xml = buildTaskXml(tr, [`<
|
|
138
|
+
const tr = `"${process.execPath}" "${script}" serve`;
|
|
139
|
+
const xml = buildTaskXml(tr, [`<BootTrigger><Enabled>true</Enabled></BootTrigger>`]);
|
|
144
140
|
const xmlPath = path.join(CONFIG_DIR, "daemon-task.xml");
|
|
145
141
|
try {
|
|
146
142
|
const bom = Buffer.from([0xFF, 0xFE]);
|
|
147
143
|
fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
|
|
148
|
-
|
|
144
|
+
// S4U LogonType requires elevation — spawn schtasks via RunAs
|
|
145
|
+
const args = `/create /tn "${tn}" /xml "${xmlPath}" /f`;
|
|
146
|
+
execFileSync("powershell", [
|
|
147
|
+
"-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '${args}'`,
|
|
148
|
+
], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
|
|
149
149
|
}
|
|
150
150
|
catch (err) {
|
|
151
151
|
const e = err;
|
|
@@ -157,6 +157,12 @@ export class WindowsPlatform {
|
|
|
157
157
|
}
|
|
158
158
|
catch { /* ignore */ }
|
|
159
159
|
}
|
|
160
|
+
// Cleanup old VBS launcher if upgrading
|
|
161
|
+
const oldVbs = path.join(CONFIG_DIR, "daemon.vbs");
|
|
162
|
+
try {
|
|
163
|
+
fs.unlinkSync(oldVbs);
|
|
164
|
+
}
|
|
165
|
+
catch { /* ignore */ }
|
|
160
166
|
}
|
|
161
167
|
/** Start the daemon via Task Scheduler (runs outside any session's job object). */
|
|
162
168
|
startDaemonTask() {
|
|
@@ -174,12 +180,7 @@ export class WindowsPlatform {
|
|
|
174
180
|
const taskId = task.frontmatter.id;
|
|
175
181
|
const tn = schtasksTaskName(taskId);
|
|
176
182
|
const script = process.argv[1] || "palmier";
|
|
177
|
-
|
|
178
|
-
const vbsPath = path.join(CONFIG_DIR, `task-${taskId}.vbs`);
|
|
179
|
-
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" run ${taskId}", 0, True`;
|
|
180
|
-
fs.writeFileSync(vbsPath, vbs, "utf-8");
|
|
181
|
-
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
182
|
-
const tr = `"${wscript}" "${vbsPath}"`;
|
|
183
|
+
const tr = `"${process.execPath}" "${script}" run ${taskId}`;
|
|
183
184
|
// Build trigger XML elements
|
|
184
185
|
const triggerElements = [];
|
|
185
186
|
if (task.frontmatter.triggers_enabled) {
|
|
@@ -198,6 +199,8 @@ export class WindowsPlatform {
|
|
|
198
199
|
}
|
|
199
200
|
// Write XML and register via schtasks — gives us full control over
|
|
200
201
|
// settings like MultipleInstancesPolicy that schtasks flags don't expose.
|
|
202
|
+
// S4U LogonType ensures no console window. Works without elevation
|
|
203
|
+
// because the daemon (which calls this) runs elevated.
|
|
201
204
|
const xml = buildTaskXml(tr, triggerElements);
|
|
202
205
|
const xmlPath = path.join(CONFIG_DIR, `task-${taskId}.xml`);
|
|
203
206
|
try {
|
|
@@ -218,6 +221,11 @@ export class WindowsPlatform {
|
|
|
218
221
|
}
|
|
219
222
|
catch { /* ignore */ }
|
|
220
223
|
}
|
|
224
|
+
// Cleanup old VBS launcher if upgrading
|
|
225
|
+
try {
|
|
226
|
+
fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`));
|
|
227
|
+
}
|
|
228
|
+
catch { /* ignore */ }
|
|
221
229
|
}
|
|
222
230
|
removeTaskTimer(taskId) {
|
|
223
231
|
const tn = schtasksTaskName(taskId);
|
|
@@ -227,10 +235,6 @@ export class WindowsPlatform {
|
|
|
227
235
|
catch {
|
|
228
236
|
// Task might not exist — that's fine
|
|
229
237
|
}
|
|
230
|
-
try {
|
|
231
|
-
fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`));
|
|
232
|
-
}
|
|
233
|
-
catch { /* ignore */ }
|
|
234
238
|
}
|
|
235
239
|
async startTask(taskId) {
|
|
236
240
|
const tn = schtasksTaskName(taskId);
|
package/package.json
CHANGED
package/src/platform/windows.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { getTaskDir, readTaskStatus } from "../task.js";
|
|
|
10
10
|
const TASK_PREFIX = "\\Palmier\\PalmierTask-";
|
|
11
11
|
const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
12
12
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
13
|
-
const DAEMON_VBS_FILE = path.join(CONFIG_DIR, "daemon.vbs");
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
@@ -68,6 +67,12 @@ export function buildTaskXml(tr: string, triggers: string[]): string {
|
|
|
68
67
|
return [
|
|
69
68
|
`<?xml version="1.0" encoding="UTF-16"?>`,
|
|
70
69
|
`<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">`,
|
|
70
|
+
` <Principals>`,
|
|
71
|
+
` <Principal>`,
|
|
72
|
+
` <LogonType>S4U</LogonType>`,
|
|
73
|
+
` <RunLevel>LeastPrivilege</RunLevel>`,
|
|
74
|
+
` </Principal>`,
|
|
75
|
+
` </Principals>`,
|
|
71
76
|
` <Settings>`,
|
|
72
77
|
` <MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>`,
|
|
73
78
|
` <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`,
|
|
@@ -93,22 +98,16 @@ export class WindowsPlatform implements PlatformService {
|
|
|
93
98
|
installDaemon(config: HostConfig): void {
|
|
94
99
|
const script = process.argv[1] || "palmier";
|
|
95
100
|
|
|
96
|
-
// Create the Task Scheduler entry for the daemon
|
|
101
|
+
// Create the Task Scheduler entry for the daemon (BootTrigger starts it at system boot)
|
|
97
102
|
this.ensureDaemonTask(script);
|
|
98
103
|
|
|
99
|
-
// Registry Run key
|
|
100
|
-
// so the daemon always runs outside any session's job object.
|
|
101
|
-
const regValue = `schtasks /run /tn "\\Palmier\\${DAEMON_TASK_NAME}"`;
|
|
104
|
+
// Remove old Registry Run key if upgrading
|
|
102
105
|
try {
|
|
103
106
|
execFileSync("reg", [
|
|
104
|
-
"
|
|
105
|
-
"/v", DAEMON_TASK_NAME, "/
|
|
107
|
+
"delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
|
|
108
|
+
"/v", DAEMON_TASK_NAME, "/f",
|
|
106
109
|
], { encoding: "utf-8", stdio: "pipe" });
|
|
107
|
-
|
|
108
|
-
} catch (err) {
|
|
109
|
-
console.error(`Warning: failed to install registry run entry: ${err}`);
|
|
110
|
-
console.error("You may need to start palmier serve manually.");
|
|
111
|
-
}
|
|
110
|
+
} catch { /* key may not exist */ }
|
|
112
111
|
|
|
113
112
|
// Start the daemon now
|
|
114
113
|
this.startDaemonTask();
|
|
@@ -152,26 +151,30 @@ export class WindowsPlatform implements PlatformService {
|
|
|
152
151
|
this.startDaemonTask();
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
/** Create or update the Task Scheduler entry for the daemon. */
|
|
154
|
+
/** Create or update the Task Scheduler entry for the daemon (requires elevation for S4U). */
|
|
156
155
|
private ensureDaemonTask(script: string): void {
|
|
157
|
-
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
|
|
158
|
-
fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
|
|
159
|
-
|
|
160
|
-
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
161
156
|
const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
|
|
162
|
-
const tr = `"${
|
|
163
|
-
const xml = buildTaskXml(tr, [`<
|
|
157
|
+
const tr = `"${process.execPath}" "${script}" serve`;
|
|
158
|
+
const xml = buildTaskXml(tr, [`<BootTrigger><Enabled>true</Enabled></BootTrigger>`]);
|
|
164
159
|
const xmlPath = path.join(CONFIG_DIR, "daemon-task.xml");
|
|
165
160
|
try {
|
|
166
161
|
const bom = Buffer.from([0xFF, 0xFE]);
|
|
167
162
|
fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
|
|
168
|
-
|
|
163
|
+
// S4U LogonType requires elevation — spawn schtasks via RunAs
|
|
164
|
+
const args = `/create /tn "${tn}" /xml "${xmlPath}" /f`;
|
|
165
|
+
execFileSync("powershell", [
|
|
166
|
+
"-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '${args}'`,
|
|
167
|
+
], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
|
|
169
168
|
} catch (err: unknown) {
|
|
170
169
|
const e = err as { stderr?: string };
|
|
171
170
|
console.error(`Failed to create daemon task: ${e.stderr || err}`);
|
|
172
171
|
} finally {
|
|
173
172
|
try { fs.unlinkSync(xmlPath); } catch { /* ignore */ }
|
|
174
173
|
}
|
|
174
|
+
|
|
175
|
+
// Cleanup old VBS launcher if upgrading
|
|
176
|
+
const oldVbs = path.join(CONFIG_DIR, "daemon.vbs");
|
|
177
|
+
try { fs.unlinkSync(oldVbs); } catch { /* ignore */ }
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
/** Start the daemon via Task Scheduler (runs outside any session's job object). */
|
|
@@ -190,14 +193,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
190
193
|
const taskId = task.frontmatter.id;
|
|
191
194
|
const tn = schtasksTaskName(taskId);
|
|
192
195
|
const script = process.argv[1] || "palmier";
|
|
193
|
-
|
|
194
|
-
// Write a VBS launcher so the task runs without a visible console window
|
|
195
|
-
const vbsPath = path.join(CONFIG_DIR, `task-${taskId}.vbs`);
|
|
196
|
-
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" run ${taskId}", 0, True`;
|
|
197
|
-
fs.writeFileSync(vbsPath, vbs, "utf-8");
|
|
198
|
-
|
|
199
|
-
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
200
|
-
const tr = `"${wscript}" "${vbsPath}"`;
|
|
196
|
+
const tr = `"${process.execPath}" "${script}" run ${taskId}`;
|
|
201
197
|
|
|
202
198
|
// Build trigger XML elements
|
|
203
199
|
const triggerElements: string[] = [];
|
|
@@ -217,6 +213,8 @@ export class WindowsPlatform implements PlatformService {
|
|
|
217
213
|
|
|
218
214
|
// Write XML and register via schtasks — gives us full control over
|
|
219
215
|
// settings like MultipleInstancesPolicy that schtasks flags don't expose.
|
|
216
|
+
// S4U LogonType ensures no console window. Works without elevation
|
|
217
|
+
// because the daemon (which calls this) runs elevated.
|
|
220
218
|
const xml = buildTaskXml(tr, triggerElements);
|
|
221
219
|
const xmlPath = path.join(CONFIG_DIR, `task-${taskId}.xml`);
|
|
222
220
|
try {
|
|
@@ -232,6 +230,9 @@ export class WindowsPlatform implements PlatformService {
|
|
|
232
230
|
} finally {
|
|
233
231
|
try { fs.unlinkSync(xmlPath); } catch { /* ignore */ }
|
|
234
232
|
}
|
|
233
|
+
|
|
234
|
+
// Cleanup old VBS launcher if upgrading
|
|
235
|
+
try { fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`)); } catch { /* ignore */ }
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
removeTaskTimer(taskId: string): void {
|
|
@@ -241,7 +242,6 @@ export class WindowsPlatform implements PlatformService {
|
|
|
241
242
|
} catch {
|
|
242
243
|
// Task might not exist — that's fine
|
|
243
244
|
}
|
|
244
|
-
try { fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`)); } catch { /* ignore */ }
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
async startTask(taskId: string): Promise<void> {
|
package/test/windows-xml.test.ts
CHANGED
|
@@ -60,6 +60,8 @@ describe("buildTaskXml", () => {
|
|
|
60
60
|
const xml = buildTaskXml(tr, triggers);
|
|
61
61
|
|
|
62
62
|
assert.ok(xml.includes('<?xml version="1.0" encoding="UTF-16"?>'), "should have XML declaration");
|
|
63
|
+
assert.ok(xml.includes("<LogonType>S4U</LogonType>"), "should use S4U logon type");
|
|
64
|
+
assert.ok(xml.includes("<RunLevel>LeastPrivilege</RunLevel>"), "should use least privilege");
|
|
63
65
|
assert.ok(xml.includes("<MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>"), "should set StopExisting");
|
|
64
66
|
assert.ok(xml.includes("<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>"), "should allow on battery");
|
|
65
67
|
assert.ok(xml.includes("<Command>C:\\Program Files\\nodejs\\node.exe</Command>"), "should extract command");
|