scripts/autostart-core.mjs
Rama main · réplica de lectura del repositorio original.
import os from "node:os";
import path from "node:path";
export const WINDOWS_TASK_NAME = "PalmTTY Agent";
export const WINDOWS_INSTALLATION_VERSION = 1;
export const LINUX_UNIT_NAME = "palmtty.service";
export function defaultConfigPath(platform = process.platform, env = process.env, home = os.homedir()) {
const platformPath = platform === "win32" ? path.win32 : path.posix;
if (platform === "win32") {
return platformPath.join(env.APPDATA ?? home, "PalmTTY", "config.yaml");
}
if (platform === "darwin") {
return platformPath.join(home, "Library", "Application Support", "PalmTTY", "config.yaml");
}
return platformPath.join(env.XDG_CONFIG_HOME ?? platformPath.join(home, ".config"), "palmtty", "config.yaml");
}
export function defaultLinuxUnitPath(env = process.env, home = os.homedir()) {
return path.join(env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "systemd", "user", LINUX_UNIT_NAME);
}
export function defaultWindowsPowerShellPath(env = process.env) {
const systemRoot = env.SystemRoot ?? env.WINDIR;
if (!systemRoot) {
throw new Error("Windows SystemRoot/WINDIR is unavailable");
}
return path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
}
export function defaultWindowsAutostartDir(env = process.env, home = os.homedir()) {
const localAppData = env.LOCALAPPDATA ?? env.APPDATA ?? home;
return path.win32.join(localAppData, "PalmTTY", "autostart");
}
export function windowsAutostartPaths(env = process.env, home = os.homedir()) {
const directory = defaultWindowsAutostartDir(env, home);
return {
directory,
host: path.win32.join(directory, "palmtty-autostart-host.exe"),
installation: path.win32.join(directory, "installation.json"),
runtime: path.win32.join(directory, "runtime.json"),
lastError: path.win32.join(directory, "last-error.txt")
};
}
export function parseAutostartArgs(argv) {
const args = [...argv];
const command = args.shift();
if (!command || !["install", "status", "restart", "uninstall"].includes(command)) {
throw new Error("Usage: pnpm autostart <install|status|restart|uninstall> [--config <path>] [--env-file <path>]");
}
if (command !== "install" && args.length > 0) {
throw new Error(`${command} does not accept install options`);
}
let configPath;
let envFile;
for (let index = 0; index < args.length; index += 1) {
const option = args[index];
if (option !== "--config" && option !== "--env-file") {
throw new Error(`Unknown autostart option: ${option}`);
}
const value = args[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`Missing value for ${option}`);
}
if (option === "--config") {
if (configPath !== undefined) throw new Error("--config may be specified only once");
configPath = path.resolve(value);
} else {
if (envFile !== undefined) throw new Error("--env-file may be specified only once");
envFile = path.resolve(value);
}
index += 1;
}
return { command, configPath, envFile };
}
export function quoteWindowsArg(value) {
if (value.length === 0) return '""';
if (!/[\s"]/u.test(value)) return value;
let result = '"';
let backslashes = 0;
for (const character of value) {
if (character === "\\") {
backslashes += 1;
continue;
}
if (character === '"') {
result += "\\".repeat(backslashes * 2 + 1) + '"';
backslashes = 0;
continue;
}
result += "\\".repeat(backslashes) + character;
backslashes = 0;
}
result += "\\".repeat(backslashes * 2) + '"';
return result;
}
export function escapeXml(value) {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function assertSingleLine(value, name) {
if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/u.test(value)) {
throw new Error(`${name} contains an unsupported control character`);
}
if (/\r|\n/u.test(value)) {
throw new Error(`${name} must be a single line`);
}
}
export function buildAgentArguments({ agentPath, configPath, envFile }) {
const args = [agentPath, "--config", configPath];
if (envFile) args.push("--env-file", envFile);
return args;
}
export function quotePowerShellLiteral(value) {
assertSingleLine(value, "PowerShell argument");
return `'${value.replaceAll("'", "''")}'`;
}
export function encodePowerShellCommand(command) {
return Buffer.from(command, "utf16le").toString("base64");
}
export function buildWindowsHostCompilePowerShellCommand({ sourcePath, outputPath }) {
assertSingleLine(sourcePath, "sourcePath");
assertSingleLine(outputPath, "outputPath");
return [
"$ErrorActionPreference = 'Stop'",
"Add-Type `",
` -Path ${quotePowerShellLiteral(sourcePath)} \``,
` -OutputAssembly ${quotePowerShellLiteral(outputPath)} \``,
" -OutputType WindowsApplication `",
" -ReferencedAssemblies 'System.dll','System.Core.dll','System.Runtime.Serialization.dll','System.Xml.dll'"
].join("\n");
}
export function buildWindowsInstallationManifest({
nodePath,
agentPath,
repoRoot,
configPath,
envFile
}) {
for (const [name, value] of Object.entries({
nodePath,
agentPath,
repoRoot,
configPath
})) {
assertSingleLine(value, name);
}
if (envFile) assertSingleLine(envFile, "envFile");
return {
version: WINDOWS_INSTALLATION_VERSION,
nodePath,
agentPath,
workingDirectory: repoRoot,
configPath,
...(envFile ? { envFile } : {})
};
}
export function parseWindowsInstallationManifest(source) {
let value;
try {
value = JSON.parse(source);
} catch {
throw new Error("Windows autostart installation manifest is invalid JSON");
}
if (!value || typeof value !== "object" || value.version !== WINDOWS_INSTALLATION_VERSION) {
throw new Error("Windows autostart installation manifest has an unsupported version");
}
for (const key of ["nodePath", "agentPath", "workingDirectory", "configPath"]) {
if (typeof value[key] !== "string" || value[key].length === 0) {
throw new Error(`Windows autostart installation manifest is missing ${key}`);
}
}
if (value.envFile !== undefined && (typeof value.envFile !== "string" || value.envFile.length === 0)) {
throw new Error("Windows autostart installation manifest has an invalid envFile");
}
return {
version: WINDOWS_INSTALLATION_VERSION,
nodePath: value.nodePath,
agentPath: value.agentPath,
workingDirectory: value.workingDirectory,
configPath: value.configPath,
...(value.envFile ? { envFile: value.envFile } : {})
};
}
export function buildWindowsTaskXml({
userSid,
hostPath,
installationPath,
workingDirectory
}) {
for (const [name, value] of Object.entries({
userSid,
hostPath,
installationPath,
workingDirectory
})) {
assertSingleLine(value, name);
}
const argumentsText = [
"--installation",
installationPath
].map(quoteWindowsArg).join(" ");
return `<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>Start PalmTTY Agent for the current user at sign-in.</Description>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
<UserId>${escapeXml(userSid)}</UserId>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>${escapeXml(userSid)}</UserId>
<LogonType>InteractiveToken</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<Priority>7</Priority>
<RestartOnFailure>
<Interval>PT1M</Interval>
<Count>3</Count>
</RestartOnFailure>
</Settings>
<Actions Context="Author">
<Exec>
<Command>${escapeXml(hostPath)}</Command>
<Arguments>${escapeXml(argumentsText)}</Arguments>
<WorkingDirectory>${escapeXml(workingDirectory)}</WorkingDirectory>
</Exec>
</Actions>
</Task>
`;
}
export function buildWindowsTaskStatusPowerShellCommand(taskName = WINDOWS_TASK_NAME) {
const taskNameLiteral = quotePowerShellLiteral(taskName);
return `[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)\n` +
`$task = Get-ScheduledTask -TaskName ${taskNameLiteral} -ErrorAction SilentlyContinue\n` +
`if ($null -eq $task) {\n` +
` [PSCustomObject]@{ installed = $false } | ConvertTo-Json -Compress\n` +
` exit 0\n` +
`}\n` +
`$info = Get-ScheduledTaskInfo -TaskName ${taskNameLiteral}\n` +
`$lastRunTime = $null\n` +
`if ($info.LastRunTime -ne [datetime]::MinValue) { $lastRunTime = $info.LastRunTime.ToUniversalTime().ToString('o') }\n` +
`$nextRunTime = $null\n` +
`if ($info.NextRunTime -ne [datetime]::MinValue) { $nextRunTime = $info.NextRunTime.ToUniversalTime().ToString('o') }\n` +
`[PSCustomObject]@{\n` +
` installed = $true\n` +
` state = [string]$task.State\n` +
` lastRunTime = $lastRunTime\n` +
` nextRunTime = $nextRunTime\n` +
` lastTaskResult = [int64]$info.LastTaskResult\n` +
`} | ConvertTo-Json -Compress`;
}
export function parseWindowsTaskStatus(output) {
let parsed;
try {
parsed = JSON.parse(output.trim());
} catch {
throw new Error("Windows Task Scheduler returned an invalid status payload");
}
if (!parsed || typeof parsed !== "object" || typeof parsed.installed !== "boolean") {
throw new Error("Windows Task Scheduler returned an invalid status payload");
}
if (!parsed.installed) return { installed: false };
if (typeof parsed.state !== "string" || parsed.state.length === 0) {
throw new Error("Windows Task Scheduler returned an invalid task state");
}
for (const key of ["lastRunTime", "nextRunTime"]) {
const value = parsed[key];
if (value !== null && value !== undefined && typeof value !== "string") {
throw new Error(`Windows Task Scheduler returned an invalid ${key}`);
}
}
if (typeof parsed.lastTaskResult !== "number" || !Number.isSafeInteger(parsed.lastTaskResult)) {
throw new Error("Windows Task Scheduler returned an invalid lastTaskResult");
}
return {
installed: true,
state: parsed.state.toLowerCase(),
lastRunTime: parsed.lastRunTime ?? null,
nextRunTime: parsed.nextRunTime ?? null,
lastTaskResult: parsed.lastTaskResult
};
}
export function quoteSystemdArg(value) {
assertSingleLine(value, "systemd argument");
return `"${value
.replaceAll("\\", "\\\\")
.replaceAll('"', '\\"')
.replaceAll("%", "%%")}"`;
}
export function buildSystemdUserUnit({ nodePath, agentPath, repoRoot, configPath, envFile }) {
const execArgs = [nodePath, ...buildAgentArguments({ agentPath, configPath, envFile })]
.map(quoteSystemdArg)
.join(" ");
return `[Unit]
Description=PalmTTY Agent
[Service]
Type=simple
WorkingDirectory=${quoteSystemdArg(repoRoot)}
ExecStart=${execArgs}
Restart=on-failure
RestartSec=3s
# Session Workers are deliberately independent from the Agent control plane.
KillMode=process
[Install]
WantedBy=default.target
`;
}