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.
@@ -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;
@@ -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 triggers the Task Scheduler entry on logon,
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
- "add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
88
- "/v", DAEMON_TASK_NAME, "/t", "REG_SZ", "/d", regValue, "/f",
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 = `"${wscript}" "${DAEMON_VBS_FILE}"`;
143
- const xml = buildTaskXml(tr, [`<TimeTrigger><StartBoundary>2000-01-01T00:00:00</StartBoundary></TimeTrigger>`]);
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
- execFileSync("schtasks", ["/create", "/tn", tn, "/xml", xmlPath, "/f"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
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
- // Write a VBS launcher so the task runs without a visible console window
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -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 triggers the Task Scheduler entry on logon,
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
- "add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
105
- "/v", DAEMON_TASK_NAME, "/t", "REG_SZ", "/d", regValue, "/f",
107
+ "delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
108
+ "/v", DAEMON_TASK_NAME, "/f",
106
109
  ], { encoding: "utf-8", stdio: "pipe" });
107
- console.log(`Registry Run key "${DAEMON_TASK_NAME}" installed (runs at logon).`);
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 = `"${wscript}" "${DAEMON_VBS_FILE}"`;
163
- const xml = buildTaskXml(tr, [`<TimeTrigger><StartBoundary>2000-01-01T00:00:00</StartBoundary></TimeTrigger>`]);
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
- execFileSync("schtasks", ["/create", "/tn", tn, "/xml", xmlPath, "/f"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
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> {
@@ -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");