@bun-native/runtime 0.0.0-stage → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +4 -2
- package/bun-native-source.json +43 -0
- package/package.json +29 -4
- package/src/exit-diagnostic.ts +136 -0
- package/src/index.ts +1060 -0
- package/src/installation.ts +46 -0
- package/src/startup-trace.ts +105 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The installed release this process was started as, from the variables that the verifying
|
|
5
|
+
* launcher sets (see docs/update-lifecycle.md). Undefined when the application was not started
|
|
6
|
+
* by that launcher, such as during development.
|
|
7
|
+
*/
|
|
8
|
+
export interface InstalledRelease {
|
|
9
|
+
/** The preserved per-user data directory, kept across updates and rollbacks. */
|
|
10
|
+
readonly dataDirectory: string
|
|
11
|
+
/** The installation root. */
|
|
12
|
+
readonly root: string
|
|
13
|
+
/** The release's manifest SHA-256. */
|
|
14
|
+
readonly release: string
|
|
15
|
+
/** True while this release awaits its health check; see `reportHealthy`. */
|
|
16
|
+
readonly healthPending: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sha256 = /^[0-9a-f]{64}$/
|
|
20
|
+
|
|
21
|
+
export function installedRelease(): InstalledRelease | undefined {
|
|
22
|
+
const { BUN_NATIVE_DATA_DIR, BUN_NATIVE_INSTALL_ROOT, BUN_NATIVE_RELEASE } = process.env
|
|
23
|
+
if (!BUN_NATIVE_DATA_DIR || !BUN_NATIVE_INSTALL_ROOT || !sha256.test(BUN_NATIVE_RELEASE ?? ''))
|
|
24
|
+
return undefined
|
|
25
|
+
return {
|
|
26
|
+
dataDirectory: BUN_NATIVE_DATA_DIR,
|
|
27
|
+
root: BUN_NATIVE_INSTALL_ROOT,
|
|
28
|
+
release: BUN_NATIVE_RELEASE!,
|
|
29
|
+
healthPending: Boolean(process.env['BUN_NATIVE_HEALTH_FILE']),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Report that this release passed the application's own health check, such as a first
|
|
35
|
+
* window opening with its data loaded. The launcher then confirms the release, so a later
|
|
36
|
+
* start no longer counts toward the automatic rollback. Returns false when no health check
|
|
37
|
+
* is pending. Call it once the application is known to work; an update that never reports
|
|
38
|
+
* is rolled back after its allowed starts.
|
|
39
|
+
*/
|
|
40
|
+
export async function reportHealthy(): Promise<boolean> {
|
|
41
|
+
const installed = installedRelease()
|
|
42
|
+
const file = process.env['BUN_NATIVE_HEALTH_FILE']
|
|
43
|
+
if (!installed?.healthPending || !file) return false
|
|
44
|
+
await writeFile(file, installed.release + '\n')
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Command, NativeEvent } from '@bun-native/protocol'
|
|
2
|
+
import { appendFileSync } from 'node:fs'
|
|
3
|
+
import { isAbsolute } from 'node:path'
|
|
4
|
+
import { redactDiagnostic } from './exit-diagnostic.ts'
|
|
5
|
+
|
|
6
|
+
const MAX_RECORDS = 256
|
|
7
|
+
|
|
8
|
+
/** Explicit diagnostic mode only. This is not a request scheduler or transport proxy. */
|
|
9
|
+
export class StartupTrace {
|
|
10
|
+
private readonly started = process.hrtime.bigint()
|
|
11
|
+
private records = 0
|
|
12
|
+
private unavailable = false
|
|
13
|
+
|
|
14
|
+
private constructor(
|
|
15
|
+
private readonly path: string,
|
|
16
|
+
private readonly nonce: string
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
static fromEnvironment(): StartupTrace | undefined {
|
|
20
|
+
if (process.env['BUN_NATIVE_STARTUP_TRACE'] !== '1') return
|
|
21
|
+
const path = process.env['BUN_NATIVE_EVIDENCE_LOG']
|
|
22
|
+
const nonce = process.env['BUN_NATIVE_EVIDENCE_NONCE']
|
|
23
|
+
if (
|
|
24
|
+
!path ||
|
|
25
|
+
!isAbsolute(path) ||
|
|
26
|
+
!nonce ||
|
|
27
|
+
Buffer.byteLength(nonce) > 128 ||
|
|
28
|
+
[...nonce].some((character) => {
|
|
29
|
+
const code = character.charCodeAt(0)
|
|
30
|
+
return code < 32 || (code >= 127 && code <= 159)
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
throw new Error('Native startup tracing requires an absolute evidence path and valid nonce')
|
|
34
|
+
return new StartupTrace(path, nonce)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
request(phase: string, command: Command, hostPid: number): void {
|
|
38
|
+
this.write(phase, {
|
|
39
|
+
host_pid: hostPid,
|
|
40
|
+
request_id: command.id,
|
|
41
|
+
command: command.type,
|
|
42
|
+
...('window_id' in command ? { window_id: command.window_id } : {}),
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
response(event: NativeEvent, hostPid: number): void {
|
|
47
|
+
if (!('id' in event)) return
|
|
48
|
+
this.write('response_received', {
|
|
49
|
+
host_pid: hostPid,
|
|
50
|
+
request_id: event.id,
|
|
51
|
+
response: event.type,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `stderr` is the bounded capture; it is recorded redacted, like exit diagnostics. */
|
|
56
|
+
failure(error: Error, stderr: string, clipped: boolean, hostPid: number): void {
|
|
57
|
+
this.write(
|
|
58
|
+
'transport_failed',
|
|
59
|
+
{
|
|
60
|
+
host_pid: hostPid,
|
|
61
|
+
error: error.message.slice(-1_024),
|
|
62
|
+
stderr_tail: redactDiagnostic(stderr, clipped, process.env),
|
|
63
|
+
},
|
|
64
|
+
true
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
finished(exitCode: number | null, stderr: string, clipped: boolean, hostPid: number): void {
|
|
69
|
+
this.write(
|
|
70
|
+
'transport_finished',
|
|
71
|
+
{
|
|
72
|
+
host_pid: hostPid,
|
|
73
|
+
exit_code: exitCode,
|
|
74
|
+
stderr_tail: redactDiagnostic(stderr, clipped, process.env),
|
|
75
|
+
},
|
|
76
|
+
true
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private write(phase: string, fields: Record<string, unknown>, terminal = false): void {
|
|
81
|
+
if (this.unavailable || (!terminal && this.records >= MAX_RECORDS)) return
|
|
82
|
+
if (!terminal && this.records === MAX_RECORDS - 1) {
|
|
83
|
+
phase = 'record_limit'
|
|
84
|
+
fields = { host_pid: fields['host_pid'], maximum_records: MAX_RECORDS }
|
|
85
|
+
}
|
|
86
|
+
const record = {
|
|
87
|
+
nonce: this.nonce,
|
|
88
|
+
stage: 'startup-diagnostic',
|
|
89
|
+
source: 'transport',
|
|
90
|
+
pid: process.pid,
|
|
91
|
+
clock: 'process_monotonic_relative',
|
|
92
|
+
elapsed_ns: String(process.hrtime.bigint() - this.started),
|
|
93
|
+
index: ++this.records,
|
|
94
|
+
phase,
|
|
95
|
+
...fields,
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
appendFileSync(this.path, JSON.stringify(record) + '\n', { mode: 0o600 })
|
|
99
|
+
} catch {
|
|
100
|
+
this.unavailable = true
|
|
101
|
+
// Do not replace the original transport result with a diagnostic I/O error.
|
|
102
|
+
console.error('Bun Native startup trace unavailable; evidence file could not be appended')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|