forked from ericc-ch/copilot-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpaths.ts
More file actions
119 lines (107 loc) · 3.78 KB
/
paths.ts
File metadata and controls
119 lines (107 loc) · 3.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import consola from "consola"
import fsSync from "node:fs"
import fs from "node:fs/promises"
import path from "node:path"
import { getHomePath } from "~/lib/runtime-config"
function computePaths(home: string) {
const APP_DIR = path.join(home, ".local", "share", "copilot-api")
return {
APP_DIR,
GITHUB_TOKEN_PATH: path.join(APP_DIR, "github_token"),
INSTANCE_LOCK_PATH: path.join(APP_DIR, "instance.lock"),
}
}
// Mutable in place so existing `import { PATHS }` consumers see updates after
// `refreshPaths()` runs at startup. (ESM live-bindings would also work for
// `let` exports, but in-place mutation is more portable across bundlers.)
export const PATHS: ReturnType<typeof computePaths> =
computePaths(getHomePath())
export function refreshPaths(): void {
const next = computePaths(getHomePath())
PATHS.APP_DIR = next.APP_DIR
PATHS.GITHUB_TOKEN_PATH = next.GITHUB_TOKEN_PATH
PATHS.INSTANCE_LOCK_PATH = next.INSTANCE_LOCK_PATH
}
export async function ensurePaths(): Promise<void> {
await fs.mkdir(PATHS.APP_DIR, { recursive: true })
await ensureFile(PATHS.GITHUB_TOKEN_PATH)
}
async function ensureFile(filePath: string): Promise<void> {
try {
await fs.access(filePath, fs.constants.W_OK)
} catch {
await fs.writeFile(filePath, "")
await fs.chmod(filePath, 0o600)
}
}
let lockReleased = false
/**
* Prevent two `start` instances from sharing the same on-disk APP_DIR (which
* would silently make them use the same GitHub account, defeating the point
* of running a multi-account pool). Writes a PID lockfile under APP_DIR; if
* one already exists with a live PID we abort with a helpful error. Stale
* locks (PID no longer running) are overwritten transparently.
*
* Pass `force` to bypass the check (useful if a previous instance died with
* SIGKILL and the heuristic is wrong somehow).
*/
export async function acquireInstanceLock(
options: { force?: boolean } = {},
): Promise<void> {
try {
const raw = await fs.readFile(PATHS.INSTANCE_LOCK_PATH, "utf8")
const existingPid = Number.parseInt(raw.trim(), 10)
if (!options.force && Number.isFinite(existingPid) && existingPid > 0) {
const alive = isPidAlive(existingPid)
if (alive) {
throw new Error(
`Another copilot-api instance (PID ${existingPid}) is already `
+ `using --home "${getHomePath()}" `
+ `(lockfile: ${PATHS.INSTANCE_LOCK_PATH}). `
+ "Use a different --home for each pool member, stop the other "
+ "instance, or pass --force to override.",
)
}
consola.warn(
`Stale instance lock for dead PID ${existingPid} found at `
+ `${PATHS.INSTANCE_LOCK_PATH}; overwriting.`,
)
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
}
await fs.writeFile(PATHS.INSTANCE_LOCK_PATH, `${process.pid}\n`)
const release = () => {
if (lockReleased) return
lockReleased = true
try {
// Best-effort: only delete the lock if it's still ours. Avoids racing
// with a later instance that may have taken over after a stale read.
const raw = fsSync.readFileSync(PATHS.INSTANCE_LOCK_PATH, "utf8")
if (Number.parseInt(raw.trim(), 10) === process.pid) {
fsSync.unlinkSync(PATHS.INSTANCE_LOCK_PATH)
}
} catch {
// ignore
}
}
process.once("exit", release)
process.once("SIGINT", () => {
release()
process.exit(130)
})
process.once("SIGTERM", () => {
release()
process.exit(143)
})
}
function isPidAlive(pid: number): boolean {
try {
process.kill(pid, 0)
return true
} catch (error) {
// EPERM means the process exists but we lack permission to signal it —
// still alive for our purposes.
return (error as NodeJS.ErrnoException).code === "EPERM"
}
}