import fs from
"node:fs/promises" ;
// launchd applies ThrottleInterval to any rapid relaunch, including
// intentional gateway restarts. Keep it low so CLI restarts and forced
// reinstalls do not stall for a full minute.
export
const LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS =
1 ;
// launchd stores plist integer values in decimal; 0o077 renders as 63 (owner-only files).
export
const LAUNCH_AGENT_UMASK_DECIMAL =
0 o077;
const plistEscape = (value: string): string =>
value
.replaceAll(
"&" ,
"&" )
.replaceAll(
"<" ,
"<" )
.replaceAll(
">" ,
">" )
.replaceAll(
'"' ,
""" )
.replaceAll(
"'" ,
"'" );
const plistUnescape = (value: string): string =>
value
.replaceAll(
"'" ,
"'" )
.replaceAll(
""" ,
'"' )
.replaceAll(
">" ,
">" )
.replaceAll(
"<" ,
"<" )
.replaceAll(
"&" ,
"&" );
const renderEnvDict = (env: Record<string, string | undefined> | undefined): string => {
if (!env) {
return "" ;
}
const entries = Object.entries(env).filter(
([, value]) =>
typeof value ===
"string" && value.trim(),
);
if (entries.length ===
0 ) {
return "" ;
}
const items = entries
.map(
([key, value]) =>
`\n <key>${plistEscape(key)}</key>\n <string>${plistEscape(value?.trim() ??
"" )}</string>`,
)
.join(
"" );
return `\n <key>EnvironmentVariables</key>\n <dict>${items}\n </dict>`;
};
export async
function readLaunchAgentProgramArgumentsFromFile(plistPath: string): Pro
mise<{
programArguments: string[];
workingDirectory?: string;
environment?: Record<string, string>;
sourcePath?: string;
} | null > {
try {
const plist = await fs.readFile(plistPath, "utf8" );
const programMatch = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/i);
if (!programMatch) {
return null ;
}
const args = Array.from(programMatch[1 ].matchAll(/<string>([\s\S]*?)<\/string>/gi)).map(
(match) => plistUnescape(match[1 ] ?? "" ).trim(),
);
const workingDirMatch = plist.match(
/<key>WorkingDirectory<\/key>\s*<string>([\s\S]*?)<\/string>/i,
);
const workingDirectory = workingDirMatch ? plistUnescape(workingDirMatch[1 ] ?? "" ).trim() : "" ;
const envMatch = plist.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/i);
const environment: Record<string, string> = {};
if (envMatch) {
for (const pair of envMatch[1 ].matchAll(
/<key>([\s\S]*?)<\/key>\s*<string>([\s\S]*?)<\/string>/gi,
)) {
const key = plistUnescape(pair[1 ] ?? "" ).trim();
if (!key) {
continue ;
}
const value = plistUnescape(pair[2 ] ?? "" ).trim();
environment[key] = value;
}
}
return {
programArguments: args.filter(Boolean ),
...(workingDirectory ? { workingDirectory } : {}),
...(Object.keys(environment).length > 0 ? { environment } : {}),
sourcePath: plistPath,
};
} catch {
return null ;
}
}
export function buildLaunchAgentPlist({
label,
comment,
programArguments,
workingDirectory,
stdoutPath,
stderrPath,
environment,
}: {
label: string;
comment?: string;
programArguments: string[];
workingDirectory?: string;
stdoutPath: string;
stderrPath: string;
environment?: Record<string, string | undefined>;
}): string {
const argsXml = programArguments
.map((arg) => `\n <string>${plistEscape(arg)}</string>`)
.join("" );
const workingDirXml = workingDirectory
? `\n <key>WorkingDirectory</key>\n <string>${plistEscape(workingDirectory)}</string>`
: "" ;
const commentXml = comment?.trim()
? `\n <key>Comment</key>\n <string>${plistEscape(comment.trim())}</string>`
: "" ;
const envXml = renderEnvDict(environment);
return `<?xml version="1.0" encoding="UTF-8" ?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd ">\n<plist version="1.0">\n <dict>\n <key>Label</key>\n <string>${plistEscape(label)}</string>\n ${commentXml}\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <true/>\n <key>ThrottleInterval</key>\n <integer>${LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS}</integer>\n <key>Umask</key>\n <integer>${LAUNCH_AGENT_UMASK_DECIMAL}</integer>\n <key>ProgramArguments</key>\n <array>${argsXml}\n </array>\n ${workingDirXml}\n <key>StandardOutPath</key>\n <string>${plistEscape(stdoutPath)}</string>\n <key>StandardErrorPath</key>\n <string>${plistEscape(stderrPath)}</string>${envXml}\n </dict>\n</plist>\n`;
}
Messung V0.5 in Prozent C=96 H=96 G=95
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-06)
¤
*© Formatika GbR, Deutschland