Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions backend/src/databaseMigrations/0008WorkerControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { DatabaseMigration } from "./types.ts";

export const workerControlMigration: DatabaseMigration = {
version: 8,
name: "worker-control",
sql: `
CREATE TABLE job_worker_control (
id INTEGER PRIMARY KEY CHECK (id = 1),
claims_paused INTEGER NOT NULL DEFAULT 0 CHECK (claims_paused IN (0, 1)),
updated_at TEXT NOT NULL
);

INSERT INTO job_worker_control (id, claims_paused, updated_at)
VALUES (1, 0, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
`,
};
2 changes: 2 additions & 0 deletions backend/src/databaseMigrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { maintenanceCoverageMigration } from "./0004MaintenanceCoverage.ts";
import { auditEventsMigration } from "./0005AuditEvents.ts";
import { multiFactorAuthenticationMigration } from "./0006MultiFactorAuthentication.ts";
import { deploymentRetentionIndexMigration } from "./0007DeploymentRetentionIndex.ts";
import { workerControlMigration } from "./0008WorkerControl.ts";
import type { DatabaseMigration } from "./types.ts";

export const databaseMigrations: readonly DatabaseMigration[] = [
Expand All @@ -15,6 +16,7 @@ export const databaseMigrations: readonly DatabaseMigration[] = [
auditEventsMigration,
multiFactorAuthenticationMigration,
deploymentRetentionIndexMigration,
workerControlMigration,
];

export interface DatabaseMigrationIdentity {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/databaseSchemaCompatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const CURRENT_DATABASE_SCHEMA_VERSION = databaseMigrations.at(-1)?.version ?? 0;
* remain bounded by the live schema.
*/
export const DASHBOARD_DATABASE_SCHEMA_COMPATIBILITY = Object.freeze({
maximum: 7,
maximum: 8,
minimum: 6,
target: CURRENT_DATABASE_SCHEMA_VERSION,
});
Expand Down
20 changes: 20 additions & 0 deletions backend/src/lib/systemdProperties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Parses the newline-delimited `key=value` format emitted by `systemctl show`.
* Blank lines are ignored, missing separators produce an empty value, and
* additional separators remain part of the value.
* @param output Bounded `systemctl show` output.
* @returns Parsed systemd properties.
*/
export function parseSystemdProperties(output: string): Map<string, string> {
return new Map(
output
.split("\n")
.filter(Boolean)
.map((line): [string, string] => {
const separator = line.indexOf("=");
return separator === -1
? [line, ""]
: [line.slice(0, separator), line.slice(separator + 1)];
})
);
}
41 changes: 13 additions & 28 deletions backend/src/managedBunRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import { resolveDashboardProjectPaths } from "./lib/dashboardPaths.ts";
import { resolveAbsoluteNonRootPath } from "./lib/safePath.ts";

const BUN_RUNTIME_VERSION_PATTERN =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][\dA-Za-z-]*))(?:\.(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][\dA-Za-z-]*)))*)?(?:\+[\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*)?$/u;
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][\dA-Za-z-]*))(?:\.(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][\dA-Za-z-]*)))*)?\+[\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*$/u;
const BUN_RUNTIME_VERSION_MAX_LENGTH = 64;
const RETIRED_RUNTIME_DIRECTORY_PATTERN =
/^\.retired-[\da-f]{8}-[\da-f]{4}-7[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/u;
const RUNTIME_CHECK_TIMEOUT_MS = 5000;
let currentRuntimeIdentity: string | undefined;
let currentRuntimeVersion: string | undefined;

export interface ManagedBunRuntimeInstallOptions {
runtimeRoot?: string;
Expand All @@ -27,7 +26,8 @@ export interface ManagedBunRuntimePruneResult {
}

/**
* Accepts only bounded, complete semantic versions that are safe as path segments.
* Accepts only bounded, revision-qualified semantic versions that are safe as
* path segments.
* @param value Candidate Bun version.
* @returns Whether the candidate is a strict Bun runtime version.
*/
Expand All @@ -41,7 +41,9 @@ export function isBunRuntimeVersion(value: string): boolean {

function assertBunRuntimeVersion(value: string): string {
if (!isBunRuntimeVersion(value)) {
throw new TypeError("Managed Bun runtime version must be valid semver");
throw new TypeError(
"Managed Bun runtime version must be revision-qualified semver"
);
}
return value;
}
Expand Down Expand Up @@ -116,20 +118,11 @@ function isSingleLinkRegularExecutable(filePath: string): boolean {
}
}

/**
* Reads a Bun executable's strict version without inheriting application secrets.
* @param executablePath Absolute executable path.
* @param argument Bun identity flag.
* @returns Reported Bun version, or undefined when verification fails.
*/
function bunExecutableReportedIdentity(
executablePath: string,
argument: "--revision" | "--version"
): string | undefined {
function readBunRevisionIdentity(executablePath: string): string | undefined {
if (!isCanonicalRegularExecutable(executablePath)) {
return undefined;
}
const result = spawnSync(executablePath, [argument], {
const result = spawnSync(executablePath, ["--revision"], {
encoding: "utf8",
env: {
LANG: "C",
Expand All @@ -151,7 +144,7 @@ function bunExecutableReportedIdentity(
* @returns Revision-qualified Bun identity, or undefined when verification fails.
*/
export function bunExecutableRuntimeIdentity(executablePath: string): string | undefined {
return bunExecutableReportedIdentity(executablePath, "--revision");
return readBunRevisionIdentity(executablePath);
}

/**
Expand All @@ -169,7 +162,7 @@ export function currentBunRuntimeIdentity(): string {
}

/**
* Checks an executable against either a revision-qualified or legacy version identity.
* Checks an executable against its revision-qualified release identity.
* @param executablePath Absolute executable path.
* @param identity Release-manifest Bun identity.
* @returns Whether the executable exactly satisfies the release identity.
Expand All @@ -181,27 +174,19 @@ export function bunExecutableMatchesRuntime(
if (!isBunRuntimeVersion(identity)) {
return false;
}
return (
bunExecutableRuntimeIdentity(executablePath) === identity ||
bunExecutableReportedIdentity(executablePath, "--version") === identity
);
return bunExecutableRuntimeIdentity(executablePath) === identity;
}

/**
* Checks whether an identity matches the Bun process running Dashboard.
* @param identity Release-manifest Bun identity.
* @returns Whether the current process satisfies the exact or legacy identity.
* @returns Whether the current process satisfies the exact identity.
*/
export function isCurrentBunRuntime(identity: string): boolean {
if (!isBunRuntimeVersion(identity)) {
return false;
}
const revision = currentBunRuntimeIdentity();
currentRuntimeVersion ??= bunExecutableReportedIdentity(
process.execPath,
"--version"
);
return identity === revision || identity === currentRuntimeVersion;
return identity === currentBunRuntimeIdentity();
}

/**
Expand Down
Loading