mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 08:02:21 +00:00
* Revert "feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)" This reverts commit783101b304. * Revert "feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)" This reverts commit38c2d1dcb9. * Revert "feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)" This reverts commit8b76683b40. * Revert "fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)" This reverts commitd3f8c5063b. * Revert "feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)" This reverts commit43d9b43d3f. * Revert "feat(orcad): supervisable server: stop requests, managed stop receipts and a lifetime that keeps its lock on failed teardown (#16741 T6-3) (#24433)" This reverts commitb093d3ab20. * Revert "feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)" This reverts commit1a9ac0e955. * Revert "feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)" This reverts commit99db2bfae4. * Revert "feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)" This reverts commit34a582bd39. * Revert "feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)" This reverts commitd53063d2b1. * Revert "feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)" This reverts commitff212dbbef. * Revert "feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)" This reverts commit92cb71765e. * Revert "feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)" This reverts commit6b36e4f85b. * Revert "feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)" This reverts commitd23ecef301. * Revert "feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)" This reverts commitdd87ae578d. * Revert "fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)" This reverts commitece9e4d2e3. * Revert "feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)" This reverts commit3fbdaba262. * Revert "feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)" This reverts commit4e8edc8872. * Revert "feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)" This reverts commit60c93263cc. * Revert "fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)" This reverts commit99e0303572. * Revert "feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)" This reverts commit817af768b0. * Revert "feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)" This reverts commitc9918931c8. * Revert "feat(relay): fence and drain file and git response streams on shutdown (#24185)" This reverts commitdc08ffeba9. * Revert "refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)" This reverts commita789233bbb. * Revert "feat(relay): route relay handlers through work admission; producer publication drain (#24181)" This reverts commit0b812bd698. * Revert "feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)" This reverts commit3aa2d3af7c. --------- Co-authored-by: m4air <m4air@Mac.localdomain>
232 lines
7.6 KiB
TypeScript
232 lines
7.6 KiB
TypeScript
// Why: rename() is atomic for readers but not durable. Without fsync on the file and its directory,
|
|
// a power loss after a successful rename can leave the old contents, or an empty inode — the same
|
|
// empty-file symptom as issue #1158, from a different cause. The .bak ring recovers it at up to an
|
|
// hour's loss; fsync stops it from happening.
|
|
|
|
import { closeSync, fsyncSync, openSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { copyFile, open, readdir, rm, stat } from 'node:fs/promises'
|
|
import { basename, dirname, join } from 'node:path'
|
|
import {
|
|
publishFileWithoutOverwrite,
|
|
renameFileWithWindowsRetry,
|
|
renameFileWithWindowsRetryAsync
|
|
} from './codex-accounts/fs-utils'
|
|
|
|
/**
|
|
* fsync a directory so a rename within it is durable. Best-effort by design: Windows cannot open a
|
|
* directory for fsync, and some filesystems reject it. The file fsync above it is the load-bearing
|
|
* part; this closes the "rename recorded but not persisted" window where the platform allows it.
|
|
*/
|
|
async function syncDirectory(directory: string): Promise<void> {
|
|
let handle: Awaited<ReturnType<typeof open>> | null = null
|
|
try {
|
|
handle = await open(directory, 'r')
|
|
await handle.sync()
|
|
} catch {
|
|
// Expected on Windows and on filesystems without directory fsync.
|
|
} finally {
|
|
await handle?.close().catch(() => {})
|
|
}
|
|
}
|
|
|
|
function syncDirectorySync(directory: string): void {
|
|
let fd: number | null = null
|
|
try {
|
|
fd = openSync(directory, 'r')
|
|
fsyncSync(fd)
|
|
} catch {
|
|
// Same platform caveats as syncDirectory.
|
|
} finally {
|
|
if (fd !== null) {
|
|
try {
|
|
closeSync(fd)
|
|
} catch {
|
|
// Nothing actionable; the fsync already happened or the open failed.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Rename an already-fsynced file and make the containing directory durable. */
|
|
export function renameDurableSync(tmpPath: string, finalPath: string): void {
|
|
renameFileWithWindowsRetry(tmpPath, finalPath)
|
|
syncDirectorySync(dirname(finalPath))
|
|
}
|
|
|
|
/** Publish an already-fsynced file without replacing a concurrently created destination. */
|
|
export function publishFileDurableSync(tmpPath: string, finalPath: string): boolean {
|
|
if (!publishFileWithoutOverwrite(tmpPath, finalPath)) {
|
|
return false
|
|
}
|
|
syncDirectorySync(dirname(finalPath))
|
|
rmSync(tmpPath)
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Rename and then fsync the containing directory. For callers that already fsynced the temp file
|
|
* themselves and need the rename made durable.
|
|
*/
|
|
export async function renameDurable(tmpPath: string, finalPath: string): Promise<void> {
|
|
await renameFileWithWindowsRetryAsync(tmpPath, finalPath)
|
|
await syncDirectory(dirname(finalPath))
|
|
}
|
|
|
|
/**
|
|
* Write `payload` to `tmpPath` and fsync it, WITHOUT publishing it. For callers that must order
|
|
* other work between "the new content is durable" and "the new content is visible" — a backup
|
|
* rotation that has to happen while the old file is still in place, for instance.
|
|
*/
|
|
export async function writeTempFileDurable(
|
|
tmpPath: string,
|
|
payload: string,
|
|
mode?: number
|
|
): Promise<void> {
|
|
const handle = await open(tmpPath, 'w', mode)
|
|
try {
|
|
await handle.writeFile(payload, 'utf-8')
|
|
await handle.sync()
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy `sourcePath` onto `finalPath` durably: a fresh inode, fsynced, then renamed into place. A
|
|
* plain copyFile can be interrupted and leave a torn destination — fatal when the destination is
|
|
* the backup someone will fall back to. Returns false when the source does not exist.
|
|
*/
|
|
export async function copyFileDurable(sourcePath: string, finalPath: string): Promise<boolean> {
|
|
const tmpPath = durableWriteTempPath(finalPath)
|
|
let renamed = false
|
|
try {
|
|
try {
|
|
// copyFile stays in the kernel — and clones the extents outright on APFS and btrfs — so
|
|
// this does not pull the whole file through the process on every commit.
|
|
await copyFile(sourcePath, tmpPath)
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
return false
|
|
}
|
|
throw error
|
|
}
|
|
const handle = await open(tmpPath, 'r+')
|
|
try {
|
|
await handle.sync()
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
await renameFileWithWindowsRetryAsync(tmpPath, finalPath)
|
|
renamed = true
|
|
await syncDirectory(dirname(finalPath))
|
|
return true
|
|
} finally {
|
|
if (!renamed) {
|
|
await rm(tmpPath, { force: true }).catch(() => {})
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Write `payload` to `tmpPath`, fsync it, then rename onto `finalPath` and fsync the directory. */
|
|
export async function writeFileDurable(
|
|
tmpPath: string,
|
|
finalPath: string,
|
|
payload: string
|
|
): Promise<void> {
|
|
await writeFileDurableIfCurrent(tmpPath, finalPath, payload, () => true)
|
|
}
|
|
|
|
/**
|
|
* `writeFileDurable` with a commit veto: `isCurrent` is consulted after the fsync and before the
|
|
* rename so a writer that was superseded mid-write doesn't publish a stale snapshot. Returns whether
|
|
* the rename happened; the temp file is removed on every path that doesn't commit, so a multi-MB
|
|
* payload can't orphan itself.
|
|
*/
|
|
export async function writeFileDurableIfCurrent(
|
|
tmpPath: string,
|
|
finalPath: string,
|
|
payload: string,
|
|
isCurrent: () => boolean
|
|
): Promise<boolean> {
|
|
let renamed = false
|
|
try {
|
|
// Why: fsync BEFORE rename. A rename that lands first can expose a zero-length file.
|
|
await writeTempFileDurable(tmpPath, payload)
|
|
if (!(await renameFileWithWindowsRetryAsync(tmpPath, finalPath, isCurrent))) {
|
|
return false
|
|
}
|
|
renamed = true
|
|
await syncDirectory(dirname(finalPath))
|
|
return true
|
|
} finally {
|
|
if (!renamed) {
|
|
await rm(tmpPath, { force: true }).catch(() => {})
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Temp path for a durable write. Shared shape so `removeStaleDurableWriteTempFiles` can reclaim orphans. */
|
|
export function durableWriteTempPath(finalPath: string): string {
|
|
return `${finalPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
|
|
}
|
|
|
|
/**
|
|
* Sweep temp files orphaned by a death between write and rename — for multi-MB payloads they would
|
|
* otherwise accumulate forever. Callers can require a minimum age to spare another live instance's
|
|
* write. This process's own temps are always skipped because deleting one would fail its rename.
|
|
*/
|
|
export async function removeStaleDurableWriteTempFiles(
|
|
finalPath: string,
|
|
options: { minimumAgeMs?: number } = {}
|
|
): Promise<void> {
|
|
const directory = dirname(finalPath)
|
|
const prefix = `${basename(finalPath)}.`
|
|
const ownPrefix = `${prefix}${process.pid}.`
|
|
try {
|
|
const names = await readdir(directory)
|
|
await Promise.all(
|
|
names
|
|
.filter(
|
|
(name) => name.startsWith(prefix) && name.endsWith('.tmp') && !name.startsWith(ownPrefix)
|
|
)
|
|
.map(async (name) => {
|
|
const path = join(directory, name)
|
|
if (options.minimumAgeMs) {
|
|
const info = await stat(path).catch(() => null)
|
|
if (!info || Date.now() - info.mtimeMs < options.minimumAgeMs) {
|
|
return
|
|
}
|
|
}
|
|
await rm(path, { force: true }).catch(() => {})
|
|
})
|
|
)
|
|
} catch {
|
|
// Directory missing or unreadable — nothing to sweep.
|
|
}
|
|
}
|
|
|
|
/** Synchronous counterpart for quit and crash paths that cannot await. */
|
|
export function writeFileDurableSync(
|
|
tmpPath: string,
|
|
finalPath: string,
|
|
payload: string | Uint8Array
|
|
): void {
|
|
let renamed = false
|
|
try {
|
|
// A Uint8Array payload is written verbatim; a string still defaults to UTF-8.
|
|
writeFileSync(tmpPath, payload)
|
|
const fd = openSync(tmpPath, 'r+')
|
|
try {
|
|
fsyncSync(fd)
|
|
} finally {
|
|
closeSync(fd)
|
|
}
|
|
renameDurableSync(tmpPath, finalPath)
|
|
renamed = true
|
|
} finally {
|
|
if (!renamed) {
|
|
rmSync(tmpPath, { force: true })
|
|
}
|
|
}
|
|
}
|