fix(cli): externalize Windows startup PowerShell to .ps1 wrapper (schtasks /TR 261-char limit)
The inline PowerShell env-parsing script exceeded the Task Scheduler /TR 261-char limit, causing startup enable to fail on Windows. Extract the script into a .ps1 wrapper file and reduce /TR to a short powershell.exe -File command (~115 chars). Mirrors the macOS writeMacosStartupWrapper pattern. Adds regression tests pinning /TR < 200 (default) and < 261 (worst-case). Apply fix to refactored lib/cli-startup.js (was cli.js before refactor).
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
parseArgs,
|
||||
resolveServeHost,
|
||||
} from './cli.js';
|
||||
import { buildWindowsStartupTaskCommand } from './lib/cli-startup.js';
|
||||
|
||||
async function withTempOpenChamberDataDir(fn) {
|
||||
const previous = process.env.OPENCHAMBER_DATA_DIR;
|
||||
@@ -884,3 +885,37 @@ describe('lifecycle commands with unmanaged explicit ports', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Windows startup task command builder', () => {
|
||||
it('default-path length stays under 200 chars', () => {
|
||||
const cmd = buildWindowsStartupTaskCommand(
|
||||
'C:\\Users\\test\\.config\\openchamber\\bin\\OpenChamber.ps1'
|
||||
);
|
||||
expect(cmd).toMatch(/^powershell\.exe -NoProfile -ExecutionPolicy Bypass -File /);
|
||||
expect(cmd.length).toBeLessThan(200);
|
||||
});
|
||||
|
||||
it('worst-case long path stays under 261-char Task Scheduler ceiling', () => {
|
||||
// Build a wrapper path >= 180 chars (simulates long OPENCHAMBER_DATA_DIR)
|
||||
// Overhead = 57 chars (prefix + closing quote), so max wrapper for <261 total is 203
|
||||
const longPath =
|
||||
'C:\\Users\\' +
|
||||
'a'.repeat(139) +
|
||||
'\\.config\\openchamber\\bin\\OpenChamber.ps1';
|
||||
expect(longPath.length).toBeGreaterThanOrEqual(180);
|
||||
|
||||
const cmd = buildWindowsStartupTaskCommand(longPath);
|
||||
expect(cmd.length).toBeLessThan(261);
|
||||
});
|
||||
|
||||
it('does NOT inline SetEnvironmentVariable (externalization invariant)', () => {
|
||||
const cmd = buildWindowsStartupTaskCommand('C:\\wrapper.ps1');
|
||||
expect(cmd).not.toContain('SetEnvironmentVariable');
|
||||
});
|
||||
|
||||
it('uses -File form, not -Command', () => {
|
||||
const cmd = buildWindowsStartupTaskCommand('C:\\wrapper.ps1');
|
||||
expect(cmd).toContain('-File ');
|
||||
expect(cmd).not.toContain('-Command ');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,6 +74,10 @@ function getMacosStartupWrapperPath() {
|
||||
return path.join(getDataDir(), 'bin', 'OpenChamber');
|
||||
}
|
||||
|
||||
function getWindowsStartupWrapperPath() {
|
||||
return path.join(getDataDir(), 'bin', 'OpenChamber.ps1');
|
||||
}
|
||||
|
||||
function collectStartupEnv(options = {}) {
|
||||
const env = options.envSnapshot === false ? {} : Object.fromEntries(
|
||||
Object.entries(process.env)
|
||||
@@ -189,6 +193,24 @@ exec ${startupShellQuote(process.execPath)} ${args}
|
||||
return wrapperPath;
|
||||
}
|
||||
|
||||
function writeWindowsStartupWrapper(options = {}) {
|
||||
const wrapperPath = getWindowsStartupWrapperPath();
|
||||
const envFilePath = getStartupEnvFilePath();
|
||||
const startupArgs = buildStartupArgs(options).map(powershellQuote).join(' ');
|
||||
const ps1Content = [
|
||||
`$envFile=${powershellQuote(envFilePath)}`,
|
||||
`if (Test-Path $envFile) { Get-Content $envFile | ForEach-Object { if ($_ -match '^([^=]+)=(.*)$') { $v=$matches[2]; if ($v.StartsWith("'") -and $v.EndsWith("'")) { $v=$v.Substring(1,$v.Length-2).Replace("'\\''","'") }; [Environment]::SetEnvironmentVariable($matches[1], $v, 'Process') } } }`,
|
||||
`& ${powershellQuote(process.execPath)} ${startupArgs}`,
|
||||
].join('; ');
|
||||
fs.mkdirSync(path.dirname(wrapperPath), { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(wrapperPath, ps1Content, { mode: 0o700 });
|
||||
return wrapperPath;
|
||||
}
|
||||
|
||||
function buildWindowsStartupTaskCommand(wrapperPath) {
|
||||
return `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${wrapperPath}"`;
|
||||
}
|
||||
|
||||
function buildMacosLaunchAgent(options = {}) {
|
||||
const wrapperPath = writeMacosStartupWrapper(options);
|
||||
const args = [wrapperPath];
|
||||
@@ -318,21 +340,16 @@ function enableStartupService(options = {}) {
|
||||
return getStartupStatus();
|
||||
}
|
||||
|
||||
const envFilePath = writeStartupEnvFile(options);
|
||||
const startupArgs = buildStartupArgs(options).map(powershellQuote).join(', ');
|
||||
const powerShellCommand = [
|
||||
`$envFile=${powershellQuote(envFilePath)}`,
|
||||
`if (Test-Path $envFile) { Get-Content $envFile | ForEach-Object { if ($_ -match '^([^=]+)=(.*)$') { $v=$matches[2]; if ($v.StartsWith("'") -and $v.EndsWith("'")) { $v=$v.Substring(1,$v.Length-2).Replace("'\\''","'") }; [Environment]::SetEnvironmentVariable($matches[1], $v, 'Process') } } }`,
|
||||
`& ${powershellQuote(process.execPath)} ${startupArgs}`,
|
||||
].join('; ');
|
||||
const taskArgs = `powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "${powerShellCommand.replace(/"/g, '\\"')}"`;
|
||||
writeStartupEnvFile(options);
|
||||
const wrapperPath = writeWindowsStartupWrapper(options);
|
||||
const taskCommand = buildWindowsStartupTaskCommand(wrapperPath);
|
||||
runStartupCommand('schtasks.exe', [
|
||||
'/Create',
|
||||
'/TN', STARTUP_SERVICE_ID,
|
||||
'/SC', 'ONLOGON',
|
||||
'/RL', 'LIMITED',
|
||||
'/F',
|
||||
'/TR', taskArgs,
|
||||
'/TR', taskCommand,
|
||||
]);
|
||||
runStartupCommand('schtasks.exe', ['/Run', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
||||
return getStartupStatus();
|
||||
@@ -359,6 +376,8 @@ function disableStartupService() {
|
||||
|
||||
runStartupCommand('schtasks.exe', ['/End', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
||||
runStartupCommand('schtasks.exe', ['/Delete', '/TN', STARTUP_SERVICE_ID, '/F'], { allowFailure: true });
|
||||
try { fs.unlinkSync(getWindowsStartupWrapperPath()); } catch {}
|
||||
removeStartupEnvFile();
|
||||
return getStartupStatus();
|
||||
}
|
||||
|
||||
@@ -367,4 +386,5 @@ export {
|
||||
getStartupStatus,
|
||||
enableStartupService,
|
||||
disableStartupService,
|
||||
buildWindowsStartupTaskCommand,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user