entire new build system for backstage
This commit is contained in:
+17
-9
@@ -98,19 +98,22 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|||||||
# Server source (Overlord-Server/dist-clients may carry a pre-built DLL from a dev build)
|
# Server source (Overlord-Server/dist-clients may carry a pre-built DLL from a dev build)
|
||||||
COPY Overlord-Server/ ./
|
COPY Overlord-Server/ ./
|
||||||
|
|
||||||
# BackstageInjection-Rust source for the cross-compile fallback (used only if no
|
# BackstageInjection-Rust source is copied into the runtime stage too so the
|
||||||
# pre-built DLL is already present in dist-clients).
|
# server can recompile + re-randomize the loader export name on demand
|
||||||
|
# (Settings -> Backstage DLL).
|
||||||
COPY BackstageInjection-Rust/ ./BackstageInjection-Rust/
|
COPY BackstageInjection-Rust/ ./BackstageInjection-Rust/
|
||||||
COPY scripts/build-backstage-dll.sh ./scripts/
|
COPY scripts/build-backstage-dll.sh ./scripts/
|
||||||
|
|
||||||
|
# Always compile the Backstage DLL from source so every image embeds a freshly
|
||||||
|
# randomized loader export name (build.rs picks a new `x<hex>` per build).
|
||||||
|
# Pass `--build-arg BACKSTAGE_FRESH=$(date +%s)` to defeat BuildKit layer
|
||||||
|
# caching and force a new random name even with unchanged source.
|
||||||
|
ARG BACKSTAGE_FRESH=
|
||||||
RUN mkdir -p dist-clients && \
|
RUN mkdir -p dist-clients && \
|
||||||
if [ -f dist-clients/BackstageInjection.x64.dll ]; then \
|
chmod +x scripts/build-backstage-dll.sh && \
|
||||||
echo "Using pre-built BackstageInjection DLL"; \
|
echo "Building BackstageInjection DLL (fresh=${BACKSTAGE_FRESH:-default})" && \
|
||||||
else \
|
BACKSTAGE_CRATE_DIR=BackstageInjection-Rust BACKSTAGE_OUT_DIR=dist-clients bash scripts/build-backstage-dll.sh || \
|
||||||
chmod +x scripts/build-backstage-dll.sh && \
|
echo "WARNING: BackstageInjection DLL build failed; runtime can rebuild on demand (Settings -> Backstage DLL)"
|
||||||
BACKSTAGE_CRATE_DIR=BackstageInjection-Rust BACKSTAGE_OUT_DIR=dist-clients bash scripts/build-backstage-dll.sh || \
|
|
||||||
echo "WARNING: BackstageInjection DLL not available (build it with cargo/MSVC on Windows)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Keep production build phases separate so BuildKit reports the exact slow or
|
# Keep production build phases separate so BuildKit reports the exact slow or
|
||||||
# failing phase and can cache each completed phase independently.
|
# failing phase and can cache each completed phase independently.
|
||||||
@@ -178,6 +181,11 @@ COPY --from=builder /app/dist-clients ./dist-clients
|
|||||||
COPY Overlord-Client/ ./Overlord-Client/
|
COPY Overlord-Client/ ./Overlord-Client/
|
||||||
RUN test -s ./Overlord-Client/third_party/nvcodec/nvEncodeAPI.h
|
RUN test -s ./Overlord-Client/third_party/nvcodec/nvEncodeAPI.h
|
||||||
|
|
||||||
|
# Rust crate + build script needed for on-demand Backstage DLL recompilation
|
||||||
|
# (re-randomized loader export) at runtime.
|
||||||
|
COPY BackstageInjection-Rust/ ./BackstageInjection-Rust/
|
||||||
|
COPY scripts/build-backstage-dll.sh ./scripts/
|
||||||
|
|
||||||
RUN mkdir -p certs data
|
RUN mkdir -p certs data
|
||||||
|
|
||||||
# Pre-seed Go module cache so first agent builds work offline.
|
# Pre-seed Go module cache so first agent builds work offline.
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ async function renderPermissionsOverview() {
|
|||||||
"system:chat",
|
"system:chat",
|
||||||
"system:appearance",
|
"system:appearance",
|
||||||
"system:thumbnails",
|
"system:thumbnails",
|
||||||
|
"system:backstage-dll",
|
||||||
"system:input-archive",
|
"system:input-archive",
|
||||||
"system:build-limits",
|
"system:build-limits",
|
||||||
"system:export-import",
|
"system:export-import",
|
||||||
@@ -3024,6 +3025,10 @@ async function init() {
|
|||||||
await loadThumbnailSettings();
|
await loadThumbnailSettings();
|
||||||
initThumbnailHandlers();
|
initThumbnailHandlers();
|
||||||
}
|
}
|
||||||
|
if (userHas("system:backstage-dll")) {
|
||||||
|
await loadBackstageDllSettings();
|
||||||
|
initBackstageDllHandlers();
|
||||||
|
}
|
||||||
if (userHas("system:health")) {
|
if (userHas("system:health")) {
|
||||||
await loadHealthStats();
|
await loadHealthStats();
|
||||||
initHealthHandlers();
|
initHealthHandlers();
|
||||||
@@ -3101,3 +3106,146 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
// ── Backstage DLL Settings ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _backstageDllPollTimer = null;
|
||||||
|
|
||||||
|
function showBackstageDllMsg(text, type, targetEl) {
|
||||||
|
const el = targetEl || document.getElementById("backstage-dll-msg");
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = text;
|
||||||
|
if (type === "error") {
|
||||||
|
el.className = "text-sm rounded-lg px-3 py-2 border border-rose-800 bg-rose-900/20 text-rose-200";
|
||||||
|
} else if (type === "info") {
|
||||||
|
el.className = "text-sm rounded-lg px-3 py-2 border border-sky-800 bg-sky-900/20 text-sky-200";
|
||||||
|
} else {
|
||||||
|
el.className = "text-sm rounded-lg px-3 py-2 border border-emerald-800 bg-emerald-900/20 text-emerald-200";
|
||||||
|
}
|
||||||
|
el.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBackstageDllStatus(status) {
|
||||||
|
if (!status) return;
|
||||||
|
const setText = (id, text) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = text;
|
||||||
|
};
|
||||||
|
setText("backstage-dll-export", status.exportName || (status.exists ? "(no randomized export?)" : "-"));
|
||||||
|
setText("backstage-dll-path", status.outputPath || "-");
|
||||||
|
setText("backstage-dll-size", formatBytes(status.sizeBytes));
|
||||||
|
setText("backstage-dll-crate", status.crateDir || "(crate not found)");
|
||||||
|
|
||||||
|
const tc = status.toolchainDetails || {};
|
||||||
|
const hint = [];
|
||||||
|
if (tc.cargoOnPath) hint.push("cargo on PATH");
|
||||||
|
if (tc.rustInstalled) hint.push("rust toolchain cached");
|
||||||
|
if (tc.rustTargetInstalled) hint.push("windows-gnu target ready");
|
||||||
|
if (tc.mingwReady) hint.push("mingw-w64 ready");
|
||||||
|
const tcEl = document.getElementById("backstage-dll-toolchain");
|
||||||
|
if (tcEl) {
|
||||||
|
tcEl.textContent = hint.length
|
||||||
|
? `Build tooling: ${hint.join(", ")}`
|
||||||
|
: "Build tooling: none detected (the first rebuild will download it into the data dir).";
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById("backstage-dll-rebuild-btn");
|
||||||
|
if (btn) btn.disabled = !!status.building;
|
||||||
|
const lastBuild = status.lastBuild;
|
||||||
|
if (lastBuild && lastBuild.finishedAt) {
|
||||||
|
showBackstageDllMsg(
|
||||||
|
`${lastBuild.ok ? "Last build succeeded" : "Last build failed"} (${formatDate(lastBuild.finishedAt)}): ${lastBuild.message}`,
|
||||||
|
lastBuild.ok ? "success" : "error",
|
||||||
|
document.getElementById("backstage-dll-lastbuild"),
|
||||||
|
);
|
||||||
|
if (status.building) {
|
||||||
|
showBackstageDllMsg("Rebuild in progress...", "info");
|
||||||
|
}
|
||||||
|
} else if (status.building) {
|
||||||
|
showBackstageDllMsg("Rebuild in progress...", "info");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBackstageDllSettings() {
|
||||||
|
if (!userHas("system:backstage-dll")) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings/backstage-dll", { credentials: "include" });
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
renderBackstageDllStatus(data.status || {});
|
||||||
|
const el = document.getElementById("backstage-dll-rebuild-startup");
|
||||||
|
if (el) el.checked = !!(data.backstageDll && data.backstageDll.rebuildOnStartup);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load backstage DLL settings", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initBackstageDllHandlers() {
|
||||||
|
const form = document.getElementById("backstage-dll-form");
|
||||||
|
if (form) form.addEventListener("submit", saveBackstageDllSettings);
|
||||||
|
const btn = document.getElementById("backstage-dll-rebuild-btn");
|
||||||
|
if (btn) btn.addEventListener("click", triggerBackstageDllRebuild);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBackstageDllSettings(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!requireUiPermission("system:backstage-dll", "Backstage DLL permission required.")) return;
|
||||||
|
const rebuildOnStartup = !!document.getElementById("backstage-dll-rebuild-startup")?.checked;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings/backstage-dll", {
|
||||||
|
method: "PUT",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ rebuildOnStartup }),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
showBackstageDllMsg(data.error || "Failed to save", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showBackstageDllMsg("Backstage DLL settings saved.", "success");
|
||||||
|
renderBackstageDllStatus(data.status || {});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save backstage DLL settings", err);
|
||||||
|
showBackstageDllMsg("Failed to save.", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerBackstageDllRebuild() {
|
||||||
|
if (!requireUiPermission("system:backstage-dll", "Backstage DLL permission required.")) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings/backstage-dll/rebuild", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
showBackstageDllMsg(data.message || data.error || "Rebuild failed to start.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showBackstageDllMsg("Rebuild started in the background.", "info");
|
||||||
|
pollBackstageDllStatus();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to start backstage DLL rebuild", err);
|
||||||
|
showBackstageDllMsg("Failed to start rebuild.", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollBackstageDllStatus() {
|
||||||
|
const tick = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings/backstage-dll", { credentials: "include" });
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
renderBackstageDllStatus(data.status || {});
|
||||||
|
if (data.status && data.status.building) {
|
||||||
|
clearTimeout(_backstageDllPollTimer);
|
||||||
|
_backstageDllPollTimer = setTimeout(tick, 2000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to poll backstage DLL status", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
clearTimeout(_backstageDllPollTimer);
|
||||||
|
tick();
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,6 +130,7 @@
|
|||||||
<a data-target="chat-settings-section" class="settings-nav-link" data-permission="system:chat" style="--accent: 56 189 248"><i class="fa-solid fa-comments w-4 text-sky-400"></i><span>Team Chat</span></a>
|
<a data-target="chat-settings-section" class="settings-nav-link" data-permission="system:chat" style="--accent: 56 189 248"><i class="fa-solid fa-comments w-4 text-sky-400"></i><span>Team Chat</span></a>
|
||||||
<a data-target="registration-section" class="settings-nav-link" data-permission="system:registration" style="--accent: 52 211 153"><i class="fa-solid fa-user-plus w-4 text-emerald-400"></i><span>Registration</span></a>
|
<a data-target="registration-section" class="settings-nav-link" data-permission="system:registration" style="--accent: 52 211 153"><i class="fa-solid fa-user-plus w-4 text-emerald-400"></i><span>Registration</span></a>
|
||||||
<a data-target="thumbnails-section" class="settings-nav-link" data-permission="system:thumbnails" style="--accent: 56 189 248"><i class="fa-solid fa-images w-4 text-sky-400"></i><span>Screenshot Thumbnails</span></a>
|
<a data-target="thumbnails-section" class="settings-nav-link" data-permission="system:thumbnails" style="--accent: 56 189 248"><i class="fa-solid fa-images w-4 text-sky-400"></i><span>Screenshot Thumbnails</span></a>
|
||||||
|
<a data-target="backstage-dll-section" class="settings-nav-link" data-permission="system:backstage-dll" style="--accent: 251 146 60"><i class="fa-solid fa-gears w-4 text-orange-400"></i><span>Backstage DLL</span></a>
|
||||||
<a data-target="input-archive-admin-section" class="settings-nav-link" data-permission="system:input-archive" style="--accent: 250 204 21"><i class="fa-solid fa-database w-4 text-yellow-400"></i><span>Input Archive Admin</span></a>
|
<a data-target="input-archive-admin-section" class="settings-nav-link" data-permission="system:input-archive" style="--accent: 250 204 21"><i class="fa-solid fa-database w-4 text-yellow-400"></i><span>Input Archive Admin</span></a>
|
||||||
<a data-target="build-rate-limit-section" class="settings-nav-link" data-permission="system:build-limits" style="--accent: 251 146 60"><i class="fa-solid fa-gauge-high w-4 text-orange-400"></i><span>Build Rate Limit</span></a>
|
<a data-target="build-rate-limit-section" class="settings-nav-link" data-permission="system:build-limits" style="--accent: 251 146 60"><i class="fa-solid fa-gauge-high w-4 text-orange-400"></i><span>Build Rate Limit</span></a>
|
||||||
<a data-target="section-appearance" class="settings-nav-link" data-permission="system:appearance" style="--accent: 232 121 249"><i class="fa-solid fa-palette w-4 text-fuchsia-400"></i><span>Branding</span></a>
|
<a data-target="section-appearance" class="settings-nav-link" data-permission="system:appearance" style="--accent: 232 121 249"><i class="fa-solid fa-palette w-4 text-fuchsia-400"></i><span>Branding</span></a>
|
||||||
@@ -1237,6 +1238,65 @@
|
|||||||
<p id="thumbnails-msg" class="hidden text-sm rounded-lg px-3 py-2 border"></p>
|
<p id="thumbnails-msg" class="hidden text-sm rounded-lg px-3 py-2 border"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Backstage DLL -->
|
||||||
|
<section id="backstage-dll-section" data-permission="system:backstage-dll" class="hidden bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4 settings-section">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-orange-500/10 border border-orange-500/30">
|
||||||
|
<i class="fa-solid fa-gears text-orange-400"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold">Backstage DLL</h2>
|
||||||
|
<p class="text-sm text-slate-400">Admin-only. Rebuild the BackstageInjection DLL to re-randomize the loader export name.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-slate-400 bg-slate-800/40 border border-slate-700/60 rounded-lg p-3">
|
||||||
|
<p>Each rebuild generates a new random <code class="text-orange-300">x<hex></code> export name for the reflective loader. The Go client and the C# build harness already locate the loader by pattern, so newly built DLLs are used without further changes. The rebuilt DLL is stored in the server data directory.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||||||
|
<div class="rounded-lg bg-slate-900 border border-slate-800 px-3 py-2">
|
||||||
|
<div class="text-slate-400">Current export name</div>
|
||||||
|
<div id="backstage-dll-export" class="font-mono text-orange-300">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-slate-900 border border-slate-800 px-3 py-2">
|
||||||
|
<div class="text-slate-400">Active DLL</div>
|
||||||
|
<div id="backstage-dll-path" class="font-mono text-xs text-slate-200 break-all">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-slate-900 border border-slate-800 px-3 py-2">
|
||||||
|
<div class="text-slate-400">Size</div>
|
||||||
|
<div id="backstage-dll-size" class="font-medium text-slate-200">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-slate-900 border border-slate-800 px-3 py-2">
|
||||||
|
<div class="text-slate-400">Rust crate</div>
|
||||||
|
<div id="backstage-dll-crate" class="font-mono text-xs text-slate-200 break-all">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="backstage-dll-toolchain" class="text-xs text-slate-400"></div>
|
||||||
|
<p id="backstage-dll-lastbuild" class="hidden text-sm rounded-lg px-3 py-2 border"></p>
|
||||||
|
|
||||||
|
<form id="backstage-dll-form" class="space-y-4">
|
||||||
|
<label class="flex items-start gap-3 p-3 rounded-lg border border-slate-700/60 bg-slate-900/40 hover:bg-slate-900/70 cursor-pointer transition-colors">
|
||||||
|
<input id="backstage-dll-rebuild-startup" type="checkbox" class="mt-1 h-4 w-4 accent-orange-500" />
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-slate-100">Recompile on server startup</div>
|
||||||
|
<div class="text-xs text-slate-400">Rebuild and re-randomize the DLL every time the server starts. Uses the persistent toolchain cache in the data dir.</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<button type="submit" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-sky-700 hover:bg-sky-600 transition-colors text-white text-sm font-medium">
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i>Save
|
||||||
|
</button>
|
||||||
|
<button type="button" id="backstage-dll-rebuild-btn" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-orange-700 hover:bg-orange-600 transition-colors text-white text-sm font-medium">
|
||||||
|
<i class="fa-solid fa-rotate"></i>Rebuild Now (re-randomize)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p id="backstage-dll-msg" class="hidden text-sm rounded-lg px-3 py-2 border"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Input Archive Admin -->
|
<!-- Input Archive Admin -->
|
||||||
<section id="input-archive-admin-section" data-permission="system:input-archive" class="hidden bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4 settings-section">
|
<section id="input-archive-admin-section" data-permission="system:input-archive" class="hidden bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4 settings-section">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|||||||
@@ -124,10 +124,13 @@ export interface Config {
|
|||||||
buildSigning: {
|
buildSigning: {
|
||||||
banlist: string[];
|
banlist: string[];
|
||||||
};
|
};
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
dashboardEnabled: boolean;
|
dashboardEnabled: boolean;
|
||||||
wallEnabled: boolean;
|
wallEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
backstageDll: {
|
||||||
|
rebuildOnStartup: boolean;
|
||||||
|
};
|
||||||
inputArchive: {
|
inputArchive: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
retentionDays: number;
|
retentionDays: number;
|
||||||
@@ -264,10 +267,13 @@ const DEFAULT_CONFIG: Config = {
|
|||||||
buildSigning: {
|
buildSigning: {
|
||||||
banlist: [],
|
banlist: [],
|
||||||
},
|
},
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
dashboardEnabled: true,
|
dashboardEnabled: true,
|
||||||
wallEnabled: true,
|
wallEnabled: true,
|
||||||
},
|
},
|
||||||
|
backstageDll: {
|
||||||
|
rebuildOnStartup: false,
|
||||||
|
},
|
||||||
inputArchive: {
|
inputArchive: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
retentionDays: 7,
|
retentionDays: 7,
|
||||||
@@ -937,7 +943,7 @@ export function loadConfig(): Config {
|
|||||||
? fileConfig.buildSigning.banlist.filter((v: unknown): v is string => typeof v === "string" && v.length > 0)
|
? fileConfig.buildSigning.banlist.filter((v: unknown): v is string => typeof v === "string" && v.length > 0)
|
||||||
: [...DEFAULT_CONFIG.buildSigning.banlist],
|
: [...DEFAULT_CONFIG.buildSigning.banlist],
|
||||||
},
|
},
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
dashboardEnabled:
|
dashboardEnabled:
|
||||||
envBoolOverride("OVERLORD_THUMBNAILS_DASHBOARD_ENABLED") ??
|
envBoolOverride("OVERLORD_THUMBNAILS_DASHBOARD_ENABLED") ??
|
||||||
(fileConfig.thumbnails?.dashboardEnabled !== undefined
|
(fileConfig.thumbnails?.dashboardEnabled !== undefined
|
||||||
@@ -949,6 +955,13 @@ export function loadConfig(): Config {
|
|||||||
? Boolean(fileConfig.thumbnails.wallEnabled)
|
? Boolean(fileConfig.thumbnails.wallEnabled)
|
||||||
: DEFAULT_CONFIG.thumbnails.wallEnabled),
|
: DEFAULT_CONFIG.thumbnails.wallEnabled),
|
||||||
},
|
},
|
||||||
|
backstageDll: {
|
||||||
|
rebuildOnStartup:
|
||||||
|
envBoolOverride("OVERLORD_BACKSTAGE_DLL_REBUILD_ON_STARTUP") ??
|
||||||
|
(fileConfig.backstageDll?.rebuildOnStartup !== undefined
|
||||||
|
? Boolean(fileConfig.backstageDll.rebuildOnStartup)
|
||||||
|
: DEFAULT_CONFIG.backstageDll.rebuildOnStartup),
|
||||||
|
},
|
||||||
inputArchive: {
|
inputArchive: {
|
||||||
enabled:
|
enabled:
|
||||||
envBoolOverride("OVERLORD_INPUT_ARCHIVE_ENABLED") ??
|
envBoolOverride("OVERLORD_INPUT_ARCHIVE_ENABLED") ??
|
||||||
@@ -1518,6 +1531,29 @@ export async function updateThumbnailsConfig(
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateBackstageDllConfig(
|
||||||
|
updates: Partial<Config["backstageDll"]>,
|
||||||
|
): Promise<Config["backstageDll"]> {
|
||||||
|
const current = getConfig();
|
||||||
|
|
||||||
|
const next: Config["backstageDll"] = {
|
||||||
|
rebuildOnStartup:
|
||||||
|
updates.rebuildOnStartup !== undefined
|
||||||
|
? Boolean(updates.rebuildOnStartup)
|
||||||
|
: current.backstageDll.rebuildOnStartup,
|
||||||
|
};
|
||||||
|
|
||||||
|
configCache = {
|
||||||
|
...current,
|
||||||
|
backstageDll: next,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileConfig = readFileConfigForUpdate();
|
||||||
|
fileConfig.backstageDll = next;
|
||||||
|
await writePersistentFileConfig(fileConfig);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
export function getBuildSigningSecrets(): BuildSigningSecrets | null {
|
export function getBuildSigningSecrets(): BuildSigningSecrets | null {
|
||||||
const dataDir = ensureDataDir();
|
const dataDir = ensureDataDir();
|
||||||
const savePath = resolve(dataDir, "save.json");
|
const savePath = resolve(dataDir, "save.json");
|
||||||
@@ -1733,11 +1769,16 @@ export async function importFullConfig(data: Record<string, any>): Promise<{ app
|
|||||||
applied.push("buildRateLimit");
|
applied.push("buildRateLimit");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.thumbnails && typeof data.thumbnails === "object") {
|
if (data.thumbnails && typeof data.thumbnails === "object") {
|
||||||
await updateThumbnailsConfig(data.thumbnails);
|
await updateThumbnailsConfig(data.thumbnails);
|
||||||
applied.push("thumbnails");
|
applied.push("thumbnails");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.backstageDll && typeof data.backstageDll === "object") {
|
||||||
|
await updateBackstageDllConfig(data.backstageDll);
|
||||||
|
applied.push("backstageDll");
|
||||||
|
}
|
||||||
|
|
||||||
if (data.inputArchive && typeof data.inputArchive === "object") {
|
if (data.inputArchive && typeof data.inputArchive === "object") {
|
||||||
await updateInputArchiveConfig(data.inputArchive);
|
await updateInputArchiveConfig(data.inputArchive);
|
||||||
applied.push("inputArchive");
|
applied.push("inputArchive");
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { handleUsersRoutes } from "./server/routes/users-routes";
|
|||||||
import { handlePermissionGroupsRoutes } from "./server/routes/permission-groups-routes";
|
import { handlePermissionGroupsRoutes } from "./server/routes/permission-groups-routes";
|
||||||
import { handleWebSocketClose, handleWebSocketMessage, handleWebSocketOpen } from "./server/routes/websocket-lifecycle-routes";
|
import { handleWebSocketClose, handleWebSocketMessage, handleWebSocketOpen } from "./server/routes/websocket-lifecycle-routes";
|
||||||
import { handleWsUpgradeRoutes } from "./server/routes/ws-upgrade-routes";
|
import { handleWsUpgradeRoutes } from "./server/routes/ws-upgrade-routes";
|
||||||
|
import { rebuildBackstageDll } from "./server/backstage-dll-manager";
|
||||||
import { handleWebrtcRoutes } from "./server/routes/webrtc-routes";
|
import { handleWebrtcRoutes } from "./server/routes/webrtc-routes";
|
||||||
import { handleRemoteDesktopRecordingRoutes } from "./server/routes/rd-recording-routes";
|
import { handleRemoteDesktopRecordingRoutes } from "./server/routes/rd-recording-routes";
|
||||||
import { isAuthorizedAgentRequest } from "./server/agent-auth";
|
import { isAuthorizedAgentRequest } from "./server/agent-auth";
|
||||||
@@ -479,6 +480,14 @@ async function startServer() {
|
|||||||
|
|
||||||
await cleanupFileTransferTempFiles(DATA_DIR);
|
await cleanupFileTransferTempFiles(DATA_DIR);
|
||||||
logger.info("[filebrowser] cleaned stale transfer temp files on startup");
|
logger.info("[filebrowser] cleaned stale transfer temp files on startup");
|
||||||
|
|
||||||
|
// Backstage (BackstageInjection) DLL: if configured, rebuild + re-randomize
|
||||||
|
// the reflective loader export name on startup without blocking the server.
|
||||||
|
if (getConfig().backstageDll.rebuildOnStartup) {
|
||||||
|
rebuildBackstageDll().catch((err) => {
|
||||||
|
logger.warn(`[backstage-dll] startup rebuild failed: ${(err as Error).message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
let tls:
|
let tls:
|
||||||
| {
|
| {
|
||||||
tlsOptions: { cert?: string; key?: string; ca?: string };
|
tlsOptions: { cert?: string; key?: string; ca?: string };
|
||||||
|
|||||||
@@ -132,6 +132,10 @@ const PERMISSIONS = {
|
|||||||
description: "Change screenshot thumbnail settings",
|
description: "Change screenshot thumbnail settings",
|
||||||
roles: ["admin"],
|
roles: ["admin"],
|
||||||
},
|
},
|
||||||
|
"system:backstage-dll": {
|
||||||
|
description: "Rebuild the BackstageInjection DLL and change its build settings",
|
||||||
|
roles: ["admin"],
|
||||||
|
},
|
||||||
"system:input-archive": {
|
"system:input-archive": {
|
||||||
description: "Change input log archive settings",
|
description: "Change input log archive settings",
|
||||||
roles: ["admin"],
|
roles: ["admin"],
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { existsSync, readFileSync, statSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { ensureDataDir } from "../paths";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { resolveRuntimeRoot } from "./runtime-paths";
|
||||||
|
|
||||||
|
export const BACKSTAGE_DLL_NAME = "BackstageInjection.x64.dll";
|
||||||
|
|
||||||
|
// Where a freshly rebuilt BackstageInjection DLL is placed. The data dir is
|
||||||
|
// used so a re-randomized (rebuild) copy survives container restarts and is
|
||||||
|
// preferred over the image-baked dist-clients copy. Overrideable for unusual
|
||||||
|
// deployments.
|
||||||
|
export function backstageDllOutputPath(): string {
|
||||||
|
const explicit = process.env.OVERLORD_BACKSTAGE_DLL_PATH?.trim();
|
||||||
|
if (explicit) return path.resolve(explicit);
|
||||||
|
return path.join(ensureDataDir(), "dist-clients", BACKSTAGE_DLL_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
function builtinDllCandidates(): string[] {
|
||||||
|
const runtimeRoot = resolveRuntimeRoot();
|
||||||
|
return [
|
||||||
|
path.resolve(runtimeRoot, "dist-clients", BACKSTAGE_DLL_NAME),
|
||||||
|
path.resolve(process.cwd(), "dist-clients", BACKSTAGE_DLL_NAME),
|
||||||
|
path.resolve(import.meta.dir, "../../dist-clients/BackstageInjection.x64.dll"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function injectionDllCandidates(): string[] {
|
||||||
|
// Fresh builds land in the data dir first; image-baked dist-clients copies
|
||||||
|
// are the offline fallback.
|
||||||
|
return [backstageDllOutputPath(), ...builtinDllCandidates()];
|
||||||
|
}
|
||||||
|
|
||||||
|
let _cachedInjectionDll: Uint8Array | null = null;
|
||||||
|
let _dllCachePath: string | null = null;
|
||||||
|
let _dllCacheMtimeMs: number = 0;
|
||||||
|
|
||||||
|
export function invalidateBackstageDll(): void {
|
||||||
|
_cachedInjectionDll = null;
|
||||||
|
_dllCachePath = null;
|
||||||
|
_dllCacheMtimeMs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInjectionDllBytes(): Uint8Array | null {
|
||||||
|
const candidates = injectionDllCandidates();
|
||||||
|
|
||||||
|
if (_dllCachePath) {
|
||||||
|
// If a freshly rebuilt DLL exists in the data dir but the cache points at
|
||||||
|
// an older path, rescan instead of serving stale bytes.
|
||||||
|
const freshOutput = backstageDllOutputPath();
|
||||||
|
if (path.resolve(_dllCachePath) === path.resolve(freshOutput) || !existsSync(freshOutput)) {
|
||||||
|
try {
|
||||||
|
const st = statSync(_dllCachePath);
|
||||||
|
if (st.mtimeMs === _dllCacheMtimeMs && _cachedInjectionDll) {
|
||||||
|
return _cachedInjectionDll;
|
||||||
|
}
|
||||||
|
_cachedInjectionDll = new Uint8Array(readFileSync(_dllCachePath));
|
||||||
|
_dllCacheMtimeMs = st.mtimeMs;
|
||||||
|
logger.info(`[backstage] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`);
|
||||||
|
return _cachedInjectionDll;
|
||||||
|
} catch {
|
||||||
|
_dllCachePath = null;
|
||||||
|
_cachedInjectionDll = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_dllCachePath = null;
|
||||||
|
_cachedInjectionDll = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dllPath of candidates) {
|
||||||
|
if (!existsSync(dllPath)) continue;
|
||||||
|
try {
|
||||||
|
const st = statSync(dllPath);
|
||||||
|
_cachedInjectionDll = new Uint8Array(readFileSync(dllPath));
|
||||||
|
_dllCachePath = dllPath;
|
||||||
|
_dllCacheMtimeMs = st.mtimeMs;
|
||||||
|
logger.info(`[backstage] loaded injection DLL from ${dllPath} (${_cachedInjectionDll.length} bytes)`);
|
||||||
|
return _cachedInjectionDll;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(`[backstage] injection DLL not found. Checked: ${candidates.join(", ")}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { getConfig } from "../config";
|
||||||
|
import { ensureToolchain, getToolchainRoot } from "./toolchain-manager";
|
||||||
|
import { resolveRuntimeRoot } from "./runtime-paths";
|
||||||
|
import {
|
||||||
|
BACKSTAGE_DLL_NAME,
|
||||||
|
backstageDllOutputPath,
|
||||||
|
invalidateBackstageDll,
|
||||||
|
} from "./backstage-dll-cache";
|
||||||
|
|
||||||
|
type SendToStream = (data: { type: "output"; text: string; level?: string }) => void;
|
||||||
|
|
||||||
|
const RUST_TARGET = "x86_64-pc-windows-gnu";
|
||||||
|
|
||||||
|
export type BackstageDllBuildState = {
|
||||||
|
startedAt: number | null;
|
||||||
|
finishedAt: number | null;
|
||||||
|
ok: boolean | null;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackstageDllStatus = {
|
||||||
|
configured: { rebuildOnStartup: boolean };
|
||||||
|
outputPath: string;
|
||||||
|
exists: boolean;
|
||||||
|
sizeBytes: number;
|
||||||
|
mtimeMs: number;
|
||||||
|
exportName: string | null;
|
||||||
|
crateDir: string | null;
|
||||||
|
toolchainReady: boolean;
|
||||||
|
toolchainDetails: {
|
||||||
|
cargoOnPath: boolean;
|
||||||
|
rustInstalled: boolean;
|
||||||
|
rustTargetInstalled: boolean;
|
||||||
|
mingwReady: boolean;
|
||||||
|
};
|
||||||
|
building: boolean;
|
||||||
|
lastBuild: BackstageDllBuildState;
|
||||||
|
};
|
||||||
|
|
||||||
|
let _building = false;
|
||||||
|
let _lastBuild: BackstageDllBuildState = {
|
||||||
|
startedAt: null,
|
||||||
|
finishedAt: null,
|
||||||
|
ok: null,
|
||||||
|
message: "No build has been run yet.",
|
||||||
|
};
|
||||||
|
|
||||||
|
function rustToolchainRoot(): string {
|
||||||
|
return path.join(getToolchainRoot(), "rust");
|
||||||
|
}
|
||||||
|
|
||||||
|
function cargoBinPath(): string | null {
|
||||||
|
const exe = process.platform === "win32" ? "cargo.exe" : "cargo";
|
||||||
|
const installed = path.join(rustToolchainRoot(), "cargo", "bin", exe);
|
||||||
|
if (fs.existsSync(installed)) return installed;
|
||||||
|
return Bun.which("cargo") ? "cargo" : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rustlibBaseDir(): string | null {
|
||||||
|
const toolchains = path.join(rustToolchainRoot(), "rustup", "toolchains");
|
||||||
|
if (!fs.existsSync(toolchains)) return null;
|
||||||
|
for (const entry of fs.readdirSync(toolchains)) {
|
||||||
|
const lib = path.join(toolchains, entry, "lib");
|
||||||
|
if (fs.existsSync(lib)) return lib;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mingwGccPath(): string | null {
|
||||||
|
const explicit = Bun.which("x86_64-w64-mingw32-gcc");
|
||||||
|
if (explicit) return explicit;
|
||||||
|
const candidate = path.join(
|
||||||
|
getToolchainRoot(),
|
||||||
|
"mingw-w64-x64",
|
||||||
|
"x86_64-w64-mingw32-cross",
|
||||||
|
"bin",
|
||||||
|
"x86_64-w64-mingw32-gcc",
|
||||||
|
);
|
||||||
|
if (fs.existsSync(candidate)) return candidate;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolchainDetails() {
|
||||||
|
const installedRustHome = fs.existsSync(path.join(rustToolchainRoot(), "rustup"));
|
||||||
|
const rustlib = installedRustHome ? rustlibBaseDir() : null;
|
||||||
|
const rustTargetInstalled =
|
||||||
|
rustlib !== null && fs.existsSync(path.join(rustlib, "rustlib", RUST_TARGET));
|
||||||
|
return {
|
||||||
|
cargoOnPath: !!Bun.which("cargo"),
|
||||||
|
rustInstalled: cargoBinPath() !== null,
|
||||||
|
rustTargetInstalled,
|
||||||
|
mingwReady: mingwGccPath() !== null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBackstageCrateDir(): string | null {
|
||||||
|
const explicit = process.env.BACKSTAGE_CRATE_DIR?.trim();
|
||||||
|
if (explicit) return path.resolve(explicit);
|
||||||
|
|
||||||
|
const runtimeRoot = resolveRuntimeRoot();
|
||||||
|
const candidates = [
|
||||||
|
path.resolve(runtimeRoot, "BackstageInjection-Rust"),
|
||||||
|
path.resolve(process.cwd(), "BackstageInjection-Rust"),
|
||||||
|
path.resolve(import.meta.dir, "../../../BackstageInjection-Rust"),
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (fs.existsSync(path.join(candidate, "Cargo.toml"))) return candidate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBackstageDllStatus(): BackstageDllStatus {
|
||||||
|
const cfg = getConfig().backstageDll;
|
||||||
|
const outputPath = backstageDllOutputPath();
|
||||||
|
const details = toolchainDetails();
|
||||||
|
|
||||||
|
let exists = false;
|
||||||
|
let sizeBytes = 0;
|
||||||
|
let mtimeMs = 0;
|
||||||
|
let exportName: string | null = null;
|
||||||
|
try {
|
||||||
|
const st = fs.statSync(outputPath);
|
||||||
|
exists = true;
|
||||||
|
sizeBytes = st.size;
|
||||||
|
mtimeMs = st.mtimeMs;
|
||||||
|
exportName = readPeExportName(outputPath);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const toolchainReady =
|
||||||
|
process.platform === "win32"
|
||||||
|
? details.cargoOnPath && details.mingwReady
|
||||||
|
: details.rustInstalled && details.mingwReady;
|
||||||
|
|
||||||
|
return {
|
||||||
|
configured: { rebuildOnStartup: cfg.rebuildOnStartup },
|
||||||
|
outputPath,
|
||||||
|
exists,
|
||||||
|
sizeBytes,
|
||||||
|
mtimeMs,
|
||||||
|
exportName,
|
||||||
|
crateDir: resolveBackstageCrateDir(),
|
||||||
|
toolchainReady,
|
||||||
|
toolchainDetails: details,
|
||||||
|
building: _building,
|
||||||
|
lastBuild: { ..._lastBuild },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rebuildBackstageDll(
|
||||||
|
sendToStream?: SendToStream,
|
||||||
|
): Promise<{ ok: boolean; message: string }> {
|
||||||
|
if (_building) {
|
||||||
|
return { ok: false, message: "A Backstage DLL rebuild is already in progress." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const send: SendToStream = sendToStream ?? (() => {});
|
||||||
|
const crateDir = resolveBackstageCrateDir();
|
||||||
|
if (!crateDir) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
"BackstageInjection-Rust crate not found. Set BACKSTAGE_CRATE_DIR or place the crate next to the server runtime root.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_building = true;
|
||||||
|
_lastBuild = { startedAt: Date.now(), finishedAt: null, ok: null, message: "Build started." };
|
||||||
|
|
||||||
|
const outPath = backstageDllOutputPath();
|
||||||
|
const outDir = path.dirname(outPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(outDir, { recursive: true });
|
||||||
|
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
const binDirs: string[] = [];
|
||||||
|
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
const rust = await ensureToolchain("rust", send);
|
||||||
|
const cargoInInstall = fs.existsSync(
|
||||||
|
path.join(rust.rootDir, "cargo", "bin", "cargo"),
|
||||||
|
);
|
||||||
|
if (cargoInInstall) {
|
||||||
|
env.RUSTUP_HOME = path.join(rust.rootDir, "rustup");
|
||||||
|
env.CARGO_HOME = path.join(rust.rootDir, "cargo");
|
||||||
|
}
|
||||||
|
binDirs.push(rust.binDir);
|
||||||
|
|
||||||
|
const mingw = await ensureToolchain("mingw-w64-x64", send);
|
||||||
|
binDirs.push(mingw.binDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
send({
|
||||||
|
type: "output",
|
||||||
|
text: `[backstage-dll] rebuilding ${BACKSTAGE_DLL_NAME} from ${crateDir} -> ${outPath}\n`,
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
logger.info(`[backstage-dll] building BackstageInjection DLL (target ${RUST_TARGET})`);
|
||||||
|
|
||||||
|
const fullEnv = pickStringEnv(process.env);
|
||||||
|
if (binDirs.length > 0) {
|
||||||
|
fullEnv.PATH = [...binDirs, fullEnv.PATH || ""].filter(Boolean).join(path.delimiter);
|
||||||
|
}
|
||||||
|
Object.assign(fullEnv, env);
|
||||||
|
|
||||||
|
const proc = Bun.spawn(
|
||||||
|
[
|
||||||
|
"cargo",
|
||||||
|
"build",
|
||||||
|
"--release",
|
||||||
|
"--target",
|
||||||
|
RUST_TARGET,
|
||||||
|
"--manifest-path",
|
||||||
|
path.join(crateDir, "Cargo.toml"),
|
||||||
|
],
|
||||||
|
{ env: fullEnv, stdout: "pipe", stderr: "pipe" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const streamLines = async (stream: ReadableStream<Uint8Array> | null) => {
|
||||||
|
if (!stream) return "";
|
||||||
|
let buf = "";
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
const text = new TextDecoder().decode(chunk);
|
||||||
|
buf += text;
|
||||||
|
for (const line of text.split("\n")) {
|
||||||
|
if (line.trim()) {
|
||||||
|
send({ type: "output", text: `[cargo] ${line}\n`, level: "info" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [stdoutText, stderrText] = await Promise.all([
|
||||||
|
streamLines(proc.stdout),
|
||||||
|
streamLines(proc.stderr),
|
||||||
|
]);
|
||||||
|
const exitCode = await proc.exited;
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
const detail = stderrText.trim() || stdoutText.trim() || `cargo exit ${exitCode}`;
|
||||||
|
throw new Error(`cargo build failed (exit ${exitCode}): ${detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const built = path.join(crateDir, "target", RUST_TARGET, "release", "BackstageInjection.dll");
|
||||||
|
if (!fs.existsSync(built)) {
|
||||||
|
throw new Error(`Expected built DLL not found at ${built}`);
|
||||||
|
}
|
||||||
|
fs.copyFileSync(built, outPath);
|
||||||
|
|
||||||
|
const exportName = readPeExportName(outPath);
|
||||||
|
invalidateBackstageDll();
|
||||||
|
|
||||||
|
_lastBuild = {
|
||||||
|
startedAt: _lastBuild.startedAt,
|
||||||
|
finishedAt: Date.now(),
|
||||||
|
ok: true,
|
||||||
|
message: `DLL rebuilt at ${outPath} (${fs.statSync(outPath).size} bytes, export: ${exportName ?? "unknown"}).`,
|
||||||
|
};
|
||||||
|
send({
|
||||||
|
type: "output",
|
||||||
|
text: `[backstage-dll] build complete: ${_lastBuild.message}\n`,
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
logger.info(`[backstage-dll] ${_lastBuild.message}`);
|
||||||
|
return { ok: true, message: _lastBuild.message };
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error?.message || String(error);
|
||||||
|
_lastBuild = {
|
||||||
|
startedAt: _lastBuild.startedAt,
|
||||||
|
finishedAt: Date.now(),
|
||||||
|
ok: false,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
send({ type: "output", text: `[backstage-dll] build failed: ${message}\n`, level: "error" });
|
||||||
|
logger.error(`[backstage-dll] rebuild failed: ${message}`);
|
||||||
|
return { ok: false, message };
|
||||||
|
} finally {
|
||||||
|
_building = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickStringEnv(env: NodeJS.ProcessEnv | Record<string, string | undefined>): Record<string, string> {
|
||||||
|
const out: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
if (typeof value === "string") out[key] = value;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
// Minimal PE export-table reader. Scans the names table for the per-build
|
||||||
|
// randomized `x<hex>` loader export emitted by build.rs.
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const EXPORT_TOKEN_RE = /^x[0-9a-f]{6}$/;
|
||||||
|
|
||||||
|
export function readPeExportName(dllPath: string): string | null {
|
||||||
|
let bytes: Uint8Array;
|
||||||
|
try {
|
||||||
|
bytes = new Uint8Array(fs.readFileSync(dllPath));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (bytes.length < 0x40 || bytes[0] !== 0x4d || bytes[1] !== 0x5a) {
|
||||||
|
// No "MZ" DOS header.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const peOffset = readU32(bytes, 0x3c);
|
||||||
|
if (peOffset + 0x18 > bytes.length) return null;
|
||||||
|
if (bytes[peOffset] !== 0x50 || bytes[peOffset + 1] !== 0x45) return null; // "PE"
|
||||||
|
|
||||||
|
const coffOffset = peOffset + 4;
|
||||||
|
const numberOfSections = readU16(bytes, coffOffset + 2);
|
||||||
|
const sizeOfOptionalHeader = readU16(bytes, coffOffset + 16);
|
||||||
|
const optOffset = coffOffset + 20;
|
||||||
|
if (optOffset + sizeOfOptionalHeader > bytes.length) return null;
|
||||||
|
|
||||||
|
const magic = readU16(bytes, optOffset);
|
||||||
|
let ddOffset: number;
|
||||||
|
if (magic === 0x20b) {
|
||||||
|
ddOffset = optOffset + 112; // PE32+
|
||||||
|
} else if (magic === 0x10b) {
|
||||||
|
ddOffset = optOffset + 96; // PE32
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (ddOffset + 8 > bytes.length) return null;
|
||||||
|
|
||||||
|
const exportRva = readU32(bytes, ddOffset);
|
||||||
|
if (exportRva === 0) return null;
|
||||||
|
|
||||||
|
const sectionOffset = optOffset + sizeOfOptionalHeader;
|
||||||
|
const sections: Array<{ va: number; vsize: number; rawPtr: number; rawSize: number }> = [];
|
||||||
|
for (let i = 0; i < numberOfSections; i++) {
|
||||||
|
const base = sectionOffset + i * 40;
|
||||||
|
if (base + 40 > bytes.length) break;
|
||||||
|
sections.push({
|
||||||
|
va: readU32(bytes, base + 12),
|
||||||
|
vsize: readU32(bytes, base + 8),
|
||||||
|
rawPtr: readU32(bytes, base + 20),
|
||||||
|
rawSize: readU32(bytes, base + 16),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rvaToOffset = (rva: number): number | null => {
|
||||||
|
const headersSize = sectionOffset + numberOfSections * 40;
|
||||||
|
if (rva < headersSize) return rva;
|
||||||
|
for (const section of sections) {
|
||||||
|
const span = Math.max(section.vsize, section.rawSize);
|
||||||
|
if (rva >= section.va && rva < section.va + span) {
|
||||||
|
return rva - section.va + section.rawPtr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportOffset = rvaToOffset(exportRva);
|
||||||
|
if (exportOffset == null || exportOffset + 40 > bytes.length) return null;
|
||||||
|
|
||||||
|
const numberOfNames = readU32(bytes, exportOffset + 24);
|
||||||
|
const namesRva = readU32(bytes, exportOffset + 32);
|
||||||
|
if (numberOfNames === 0 || namesRva === 0) return null;
|
||||||
|
|
||||||
|
const namesOffset = rvaToOffset(namesRva);
|
||||||
|
if (namesOffset == null) return null;
|
||||||
|
|
||||||
|
for (let i = 0; i < numberOfNames; i++) {
|
||||||
|
const entryPos = namesOffset + i * 4;
|
||||||
|
if (entryPos + 4 > bytes.length) break;
|
||||||
|
const nameRva = readU32(bytes, entryPos);
|
||||||
|
const nameOffset = rvaToOffset(nameRva);
|
||||||
|
if (nameOffset == null) continue;
|
||||||
|
let end = nameOffset;
|
||||||
|
while (end < bytes.length && bytes[end] !== 0) end++;
|
||||||
|
if (end === bytes.length) continue;
|
||||||
|
const name = new TextDecoder().decode(bytes.subarray(nameOffset, end));
|
||||||
|
if (EXPORT_TOKEN_RE.test(name)) return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readU16(bytes: Uint8Array, offset: number): number {
|
||||||
|
return bytes[offset] | (bytes[offset + 1] << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readU32(bytes: Uint8Array, offset: number): number {
|
||||||
|
return (
|
||||||
|
bytes[offset] |
|
||||||
|
(bytes[offset + 1] << 8) |
|
||||||
|
(bytes[offset + 2] << 16) |
|
||||||
|
(bytes[offset + 3] << 24)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
import { getConfig, updateBackstageDllConfig } from "../../config";
|
||||||
|
import { generateToken } from "../../auth";
|
||||||
|
import { createUser, deleteUser, getUserById } from "../../users";
|
||||||
|
import { handleMiscRoutes } from "./misc-routes";
|
||||||
|
|
||||||
|
const root = `${process.env.TEMP || "."}/overlord-backstage-dll-route-${Date.now().toString(36)}`;
|
||||||
|
let adminUserId = 0;
|
||||||
|
let adminToken = "";
|
||||||
|
let viewerUserId = 0;
|
||||||
|
let viewerToken = "";
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
CORS_HEADERS: {},
|
||||||
|
SERVER_VERSION: "3.0.0",
|
||||||
|
PUBLIC_ROOT: ".",
|
||||||
|
DATA_DIR: root,
|
||||||
|
PLUGIN_ROOT: `${root}/plugins`,
|
||||||
|
tlsCertPath: `${root}/certs/server.crt`,
|
||||||
|
tlsKeyPath: `${root}/certs/server.key`,
|
||||||
|
tlsSource: "self-signed" as const,
|
||||||
|
requestIP: () => ({ address: "127.0.0.1" }),
|
||||||
|
getConsoleSessionCount: () => 0,
|
||||||
|
getRdSessionCount: () => 0,
|
||||||
|
getFileBrowserSessionCount: () => 0,
|
||||||
|
getProcessSessionCount: () => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const admin = await createUser(
|
||||||
|
`backstage_dll_admin_${Date.now().toString(36)}`,
|
||||||
|
"Aa1!BackstageDllAdmin_2026",
|
||||||
|
"admin",
|
||||||
|
"test",
|
||||||
|
);
|
||||||
|
if (!admin.success || !admin.userId) throw new Error("failed to create admin test user");
|
||||||
|
adminUserId = admin.userId;
|
||||||
|
adminToken = await generateToken(getUserById(adminUserId)!);
|
||||||
|
|
||||||
|
const viewer = await createUser(
|
||||||
|
`backstage_dll_viewer_${Date.now().toString(36)}`,
|
||||||
|
"Aa1!BackstageDllViewer_2026",
|
||||||
|
"viewer",
|
||||||
|
"test",
|
||||||
|
);
|
||||||
|
if (!viewer.success || !viewer.userId) throw new Error("failed to create viewer test user");
|
||||||
|
viewerUserId = viewer.userId;
|
||||||
|
viewerToken = await generateToken(getUserById(viewerUserId)!);
|
||||||
|
|
||||||
|
// Start from a known state so the test is order-independent.
|
||||||
|
await updateBackstageDllConfig({ rebuildOnStartup: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await updateBackstageDllConfig({ rebuildOnStartup: false });
|
||||||
|
if (adminUserId) deleteUser(adminUserId);
|
||||||
|
if (viewerUserId) deleteUser(viewerUserId);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("backstage DLL settings routes", () => {
|
||||||
|
test("GET returns config and current status for any authenticated user", async () => {
|
||||||
|
const url = new URL("https://localhost/api/settings/backstage-dll");
|
||||||
|
const res = await handleMiscRoutes(
|
||||||
|
new Request(url, { headers: { Cookie: `overlord_token=${viewerToken}` } }),
|
||||||
|
url,
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
expect(res?.status).toBe(200);
|
||||||
|
const body: any = await res!.json();
|
||||||
|
expect(body.backstageDll).toEqual({ rebuildOnStartup: false });
|
||||||
|
expect(body.status.outputPath).toBeTruthy();
|
||||||
|
expect(typeof body.status.exists).toBe("boolean");
|
||||||
|
expect(typeof body.status.building).toBe("boolean");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GET without a session is unauthorized", async () => {
|
||||||
|
const url = new URL("https://localhost/api/settings/backstage-dll");
|
||||||
|
const res = await handleMiscRoutes(new Request(url), url, deps);
|
||||||
|
expect(res?.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("admin can toggle rebuildOnStartup", async () => {
|
||||||
|
const url = new URL("https://localhost/api/settings/backstage-dll");
|
||||||
|
const res = await handleMiscRoutes(
|
||||||
|
new Request(url, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Cookie: `overlord_token=${adminToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ rebuildOnStartup: true }),
|
||||||
|
}),
|
||||||
|
url,
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
expect(res?.status).toBe(200);
|
||||||
|
const body: any = await res!.json();
|
||||||
|
expect(body.backstageDll.rebuildOnStartup).toBe(true);
|
||||||
|
expect(getConfig().backstageDll.rebuildOnStartup).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("viewer cannot toggle rebuildOnStartup", async () => {
|
||||||
|
const url = new URL("https://localhost/api/settings/backstage-dll");
|
||||||
|
const res = await handleMiscRoutes(
|
||||||
|
new Request(url, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Cookie: `overlord_token=${viewerToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ rebuildOnStartup: true }),
|
||||||
|
}),
|
||||||
|
url,
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
expect(res?.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("admin can start a rebuild", async () => {
|
||||||
|
const url = new URL("https://localhost/api/settings/backstage-dll/rebuild");
|
||||||
|
const res = await handleMiscRoutes(
|
||||||
|
new Request(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Cookie: `overlord_token=${adminToken}` },
|
||||||
|
}),
|
||||||
|
url,
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
expect(res?.status).toBe(200);
|
||||||
|
const body: any = await res!.json();
|
||||||
|
expect(body.ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("viewer cannot start a rebuild", async () => {
|
||||||
|
const url = new URL("https://localhost/api/settings/backstage-dll/rebuild");
|
||||||
|
const res = await handleMiscRoutes(
|
||||||
|
new Request(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Cookie: `overlord_token=${viewerToken}` },
|
||||||
|
}),
|
||||||
|
url,
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
expect(res?.status).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { authenticateRequest } from "../../auth";
|
import { authenticateRequest } from "../../auth";
|
||||||
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
|
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
|
||||||
import { logger } from "../../logger";
|
import { logger } from "../../logger";
|
||||||
import { getConfig, updateSecurityConfig, updateTlsConfig, updateOidcConfig, updateAppearanceConfig, updateChatConfig, importFullConfig, updateRegistrationConfig, updateBuildRateLimitConfig, updateThumbnailsConfig, updateInputArchiveConfig, updateFileTransfersConfig } from "../../config";
|
import { getConfig, updateSecurityConfig, updateTlsConfig, updateOidcConfig, updateAppearanceConfig, updateChatConfig, importFullConfig, updateRegistrationConfig, updateBuildRateLimitConfig, updateThumbnailsConfig, updateInputArchiveConfig, updateFileTransfersConfig, updateBackstageDllConfig } from "../../config";
|
||||||
|
import { getBackstageDllStatus, rebuildBackstageDll } from "../backstage-dll-manager";
|
||||||
import {
|
import {
|
||||||
saveBrandingImage,
|
saveBrandingImage,
|
||||||
getClientMetricsSummary,
|
getClientMetricsSummary,
|
||||||
@@ -1567,6 +1568,82 @@ export async function handleMiscRoutes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Backstage DLL management (GET: any user, PUT/POST: admin only) ─────
|
||||||
|
if (url.pathname === "/api/settings/backstage-dll") {
|
||||||
|
const user = await authenticateRequest(req);
|
||||||
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
||||||
|
|
||||||
|
if (req.method === "GET") {
|
||||||
|
return Response.json(
|
||||||
|
{ backstageDll: getConfig().backstageDll, status: getBackstageDllStatus() },
|
||||||
|
{ headers: deps.CORS_HEADERS },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "PUT") {
|
||||||
|
try { requirePermission(user, "system:backstage-dll"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
||||||
|
|
||||||
|
let body: any = {};
|
||||||
|
try { body = await req.json(); } catch {
|
||||||
|
return Response.json({ error: "Invalid JSON" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await updateBackstageDllConfig({
|
||||||
|
rebuildOnStartup:
|
||||||
|
body?.rebuildOnStartup !== undefined ? Boolean(body.rebuildOnStartup) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
logAudit({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
username: user.username,
|
||||||
|
ip: deps.requestIP?.(req)?.address || "unknown",
|
||||||
|
action: AuditAction.COMMAND,
|
||||||
|
details: `Updated Backstage DLL settings (rebuildOnStartup=${updated.rebuildOnStartup})`,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{ ok: true, backstageDll: updated, status: getBackstageDllStatus() },
|
||||||
|
{ headers: deps.CORS_HEADERS },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/settings/backstage-dll/rebuild
|
||||||
|
if (req.method === "POST" && url.pathname === "/api/settings/backstage-dll/rebuild") {
|
||||||
|
const user = await authenticateRequest(req);
|
||||||
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
||||||
|
try { requirePermission(user, "system:backstage-dll"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
||||||
|
|
||||||
|
const status = getBackstageDllStatus();
|
||||||
|
if (status.building) {
|
||||||
|
return Response.json(
|
||||||
|
{ ok: false, message: "A rebuild is already in progress." },
|
||||||
|
{ status: 409, headers: deps.CORS_HEADERS },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logAudit({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
username: user.username,
|
||||||
|
ip: deps.requestIP?.(req)?.address || "unknown",
|
||||||
|
action: AuditAction.COMMAND,
|
||||||
|
details: "Triggered BackstageInjection DLL rebuild (re-randomize loader export)",
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kick off in the background so the HTTP request returns immediately; the
|
||||||
|
// UI polls GET /api/settings/backstage-dll for progress.
|
||||||
|
rebuildBackstageDll().catch((error: any) => {
|
||||||
|
logger.error(`[backstage-dll] background rebuild error: ${error?.message || error}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{ ok: true, message: "Rebuild started in the background.", status },
|
||||||
|
{ headers: deps.CORS_HEADERS },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/settings/health
|
// GET /api/settings/health
|
||||||
if (req.method === "GET" && url.pathname === "/api/settings/health") {
|
if (req.method === "GET" && url.pathname === "/api/settings/health") {
|
||||||
const user = await authenticateRequest(req);
|
const user = await authenticateRequest(req);
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export type ToolchainKey =
|
|||||||
| "linux-musl-armv7"
|
| "linux-musl-armv7"
|
||||||
| "android-ndk"
|
| "android-ndk"
|
||||||
| "ldid"
|
| "ldid"
|
||||||
| "upx";
|
| "upx"
|
||||||
|
| "rust";
|
||||||
|
|
||||||
export type EnsuredToolchain = {
|
export type EnsuredToolchain = {
|
||||||
key: ToolchainKey;
|
key: ToolchainKey;
|
||||||
@@ -25,7 +26,7 @@ export type EnsuredToolchain = {
|
|||||||
rootDir: string;
|
rootDir: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ArchiveFormat = "tar.gz" | "tar.xz" | "zip" | "binary";
|
type ArchiveFormat = "tar.gz" | "tar.xz" | "zip" | "binary" | "rustup";
|
||||||
|
|
||||||
type Manifest = {
|
type Manifest = {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -37,6 +38,7 @@ type Manifest = {
|
|||||||
cxxBasename?: string;
|
cxxBasename?: string;
|
||||||
arBasename?: string;
|
arBasename?: string;
|
||||||
binaryFilename?: string;
|
binaryFilename?: string;
|
||||||
|
pathBasename?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ANDROID_NDK_VERSION = "r27c";
|
const ANDROID_NDK_VERSION = "r27c";
|
||||||
@@ -49,6 +51,28 @@ function ldidArch(): string {
|
|||||||
return process.arch === "arm64" ? "aarch64" : "x86_64";
|
return process.arch === "arm64" ? "aarch64" : "x86_64";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rust toolchain pinned to the same version the Docker builder installs.
|
||||||
|
const RUST_VERSION = "1.85.0";
|
||||||
|
|
||||||
|
// The windows-gnu std is fetched from the rustup dist server regardless of host
|
||||||
|
// triple via `--target x86_64-pc-windows-gnu`; only the rustup-init binary is
|
||||||
|
// host-specific.
|
||||||
|
const RUST_TARGET = "x86_64-pc-windows-gnu";
|
||||||
|
|
||||||
|
function rustupInitUrl(): string {
|
||||||
|
const platform = process.platform;
|
||||||
|
const arch = process.arch;
|
||||||
|
let triple: string;
|
||||||
|
if (platform === "win32") {
|
||||||
|
triple = arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc";
|
||||||
|
} else if (platform === "darwin") {
|
||||||
|
triple = arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
|
||||||
|
} else {
|
||||||
|
triple = arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu";
|
||||||
|
}
|
||||||
|
return `https://static.rust-lang.org/rustup/dist/${triple}/rustup-init${platform === "win32" ? ".exe" : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
const MANIFESTS: Record<ToolchainKey, Manifest> = {
|
const MANIFESTS: Record<ToolchainKey, Manifest> = {
|
||||||
"mingw-w64-x64": {
|
"mingw-w64-x64": {
|
||||||
displayName: "mingw-w64 x86_64 cross-compiler",
|
displayName: "mingw-w64 x86_64 cross-compiler",
|
||||||
@@ -124,6 +148,14 @@ const MANIFESTS: Record<ToolchainKey, Manifest> = {
|
|||||||
binSubdir: "upx-4.2.4-amd64_linux",
|
binSubdir: "upx-4.2.4-amd64_linux",
|
||||||
binaryFilename: "upx",
|
binaryFilename: "upx",
|
||||||
},
|
},
|
||||||
|
rust: {
|
||||||
|
displayName: `Rust toolchain ${RUST_VERSION} (rustup minimal, ${RUST_TARGET} target)`,
|
||||||
|
url: rustupInitUrl(),
|
||||||
|
approxSizeMB: 260,
|
||||||
|
archive: "rustup",
|
||||||
|
binSubdir: "cargo/bin",
|
||||||
|
pathBasename: "cargo",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const inFlight = new Map<ToolchainKey, Promise<EnsuredToolchain>>();
|
const inFlight = new Map<ToolchainKey, Promise<EnsuredToolchain>>();
|
||||||
@@ -237,6 +269,35 @@ async function downloadAndExtract(
|
|||||||
fs.chmodSync(finalBin, 0o755);
|
fs.chmodSync(finalBin, 0o755);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "rustup": {
|
||||||
|
// rustup-init is an installer, not an archive. Rename the download to its
|
||||||
|
// canonical name and run it with RUSTUP_HOME/CARGO_HOME pointing inside
|
||||||
|
// tmpDir, so the whole toolchain tree is atomically published into the
|
||||||
|
// data dir afterwards by the standard rename below.
|
||||||
|
const exeName = process.platform === "win32" ? "rustup-init.exe" : "rustup-init";
|
||||||
|
const initPath = path.join(tmpDir, exeName);
|
||||||
|
fs.renameSync(archivePath, initPath);
|
||||||
|
if (process.platform !== "win32") fs.chmodSync(initPath, 0o755);
|
||||||
|
const rustupHome = path.join(tmpDir, "rustup");
|
||||||
|
const cargoHome = path.join(tmpDir, "cargo");
|
||||||
|
fs.mkdirSync(rustupHome, { recursive: true });
|
||||||
|
fs.mkdirSync(cargoHome, { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(cargoHome, "bin"), { recursive: true });
|
||||||
|
send({
|
||||||
|
type: "output",
|
||||||
|
text: `[toolchain] Installing ${m.displayName} (downloads Rust std + ${RUST_TARGET} target)...\n`,
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
const r = await $`${initPath} -y --no-modify-path --default-toolchain ${RUST_VERSION} --profile minimal --target ${RUST_TARGET}`
|
||||||
|
.env({ RUSTUP_HOME: rustupHome, CARGO_HOME: cargoHome })
|
||||||
|
.nothrow();
|
||||||
|
if (r.exitCode !== 0) {
|
||||||
|
const detail =
|
||||||
|
r.stderr.toString().trim() || r.stdout.toString().trim() || "no output";
|
||||||
|
throw new Error(`rustup-init install failed for ${key} (exit ${r.exitCode}): ${detail}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atomic publish: rename tmpDir → rootDir. If another process already
|
// Atomic publish: rename tmpDir → rootDir. If another process already
|
||||||
@@ -265,7 +326,7 @@ async function downloadAndExtract(
|
|||||||
|
|
||||||
function tryResolveFromPath(key: ToolchainKey): EnsuredToolchain | null {
|
function tryResolveFromPath(key: ToolchainKey): EnsuredToolchain | null {
|
||||||
const m = MANIFESTS[key];
|
const m = MANIFESTS[key];
|
||||||
const primaryBin = m.ccBasename || m.binaryFilename;
|
const primaryBin = m.pathBasename || m.ccBasename || m.binaryFilename;
|
||||||
if (!primaryBin) return null;
|
if (!primaryBin) return null;
|
||||||
|
|
||||||
const found = Bun.which(primaryBin);
|
const found = Bun.which(primaryBin);
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import type { ServerWebSocket } from "bun";
|
import type { ServerWebSocket } from "bun";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { existsSync, readFileSync } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import * as clientManager from "../clientManager";
|
import * as clientManager from "../clientManager";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { negotiateDesktopCodec } from "./desktop-codec-negotiation";
|
import { negotiateDesktopCodec } from "./desktop-codec-negotiation";
|
||||||
import { metrics } from "../metrics";
|
import { metrics } from "../metrics";
|
||||||
import { encodeMessage } from "../protocol";
|
import { encodeMessage } from "../protocol";
|
||||||
import { resolveRuntimeRoot } from "./runtime-paths";
|
import { getInjectionDllBytes } from "./backstage-dll-cache";
|
||||||
import * as sessionManager from "../sessions/sessionManager";
|
import * as sessionManager from "../sessions/sessionManager";
|
||||||
import type { ConsoleSession, RemoteDesktopViewer, SocketData } from "../sessions/types";
|
import type { ConsoleSession, RemoteDesktopViewer, SocketData } from "../sessions/types";
|
||||||
import type { ClientInfo } from "../types";
|
import type { ClientInfo } from "../types";
|
||||||
@@ -34,54 +32,6 @@ import {
|
|||||||
stopRemoteDesktopRecording,
|
stopRemoteDesktopRecording,
|
||||||
} from "./rd-recording";
|
} from "./rd-recording";
|
||||||
|
|
||||||
let _cachedInjectionDll: Uint8Array | null = null;
|
|
||||||
let _dllCachePath: string | null = null;
|
|
||||||
let _dllCacheMtimeMs: number = 0;
|
|
||||||
|
|
||||||
function getInjectionDllBytes(): Uint8Array | null {
|
|
||||||
const runtimeRoot = resolveRuntimeRoot();
|
|
||||||
const candidates = [
|
|
||||||
path.resolve(runtimeRoot, "dist-clients", "BackstageInjection.x64.dll"),
|
|
||||||
path.resolve(process.cwd(), "dist-clients", "BackstageInjection.x64.dll"),
|
|
||||||
path.resolve(import.meta.dir, "../../dist-clients/BackstageInjection.x64.dll"),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (_dllCachePath) {
|
|
||||||
try {
|
|
||||||
const { statSync } = require("fs");
|
|
||||||
const st = statSync(_dllCachePath);
|
|
||||||
if (st.mtimeMs === _dllCacheMtimeMs && _cachedInjectionDll) {
|
|
||||||
return _cachedInjectionDll;
|
|
||||||
}
|
|
||||||
_cachedInjectionDll = new Uint8Array(readFileSync(_dllCachePath));
|
|
||||||
_dllCacheMtimeMs = st.mtimeMs;
|
|
||||||
logger.info(`[backstage] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`);
|
|
||||||
return _cachedInjectionDll;
|
|
||||||
} catch {
|
|
||||||
_dllCachePath = null;
|
|
||||||
_cachedInjectionDll = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const dllPath of candidates) {
|
|
||||||
if (!existsSync(dllPath)) continue;
|
|
||||||
try {
|
|
||||||
const { statSync } = require("fs");
|
|
||||||
const st = statSync(dllPath);
|
|
||||||
_cachedInjectionDll = new Uint8Array(readFileSync(dllPath));
|
|
||||||
_dllCachePath = dllPath;
|
|
||||||
_dllCacheMtimeMs = st.mtimeMs;
|
|
||||||
logger.info(`[backstage] loaded injection DLL from ${dllPath} (${_cachedInjectionDll.length} bytes)`);
|
|
||||||
return _cachedInjectionDll;
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warn(`[backstage] injection DLL not found. Checked: ${candidates.join(", ")}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VIEWER_BACKPRESSURE_BYTES = Math.max(
|
const VIEWER_BACKPRESSURE_BYTES = Math.max(
|
||||||
64 * 1024,
|
64 * 1024,
|
||||||
Number(process.env.OVERLORD_MEDIA_VIEWER_BACKPRESSURE_BYTES || 512 * 1024),
|
Number(process.env.OVERLORD_MEDIA_VIEWER_BACKPRESSURE_BYTES || 512 * 1024),
|
||||||
|
|||||||
Reference in New Issue
Block a user