node v2 cli support (#36309)
This commit is contained in:
parent
27e1692848
commit
a1b274e6f8
75 changed files with 1502 additions and 367 deletions
|
|
@ -11,10 +11,12 @@ import { Flag } from "../flag/flag"
|
|||
import { lazy } from "../util/lazy"
|
||||
import { watch as watchFileSystem } from "node:fs"
|
||||
import path from "path"
|
||||
import { createRequire } from "node:module"
|
||||
|
||||
declare const OPENCODE_LIBC: string | undefined
|
||||
|
||||
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
export const Event = { Updated: FileSystem.Event.Changed }
|
||||
|
||||
|
|
@ -22,7 +24,8 @@ const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
|
|||
try {
|
||||
const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
|
||||
const binding = require(
|
||||
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`,
|
||||
process.env.OPENCODE_PARCEL_WATCHER_PATH ??
|
||||
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`,
|
||||
)
|
||||
return createWrapper(binding) as typeof import("@parcel/watcher")
|
||||
} catch {
|
||||
|
|
|
|||
4
packages/core/src/image/photon-wasm.bun.ts
Normal file
4
packages/core/src/image/photon-wasm.bun.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// @ts-ignore Bun embeds static file imports when compiling the CLI.
|
||||
import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" }
|
||||
|
||||
export default photonWasm
|
||||
4
packages/core/src/image/photon-wasm.node.ts
Normal file
4
packages/core/src/image/photon-wasm.node.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { createRequire } from "node:module"
|
||||
|
||||
export default process.env.OPENCODE_PHOTON_WASM_PATH ??
|
||||
createRequire(import.meta.url).resolve("@silvia-odwyer/photon-node/photon_rs_bg.wasm")
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
// @ts-ignore Bun's static file import is embedded by `bun build --compile`; some consumers also declare *.wasm.
|
||||
import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" }
|
||||
import photonWasm from "#photon-wasm"
|
||||
import { Effect } from "effect"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
|
|
|||
18
packages/core/src/node-ffi.d.ts
vendored
Normal file
18
packages/core/src/node-ffi.d.ts
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
declare module "node:ffi" {
|
||||
type Signature = {
|
||||
readonly arguments?: readonly string[]
|
||||
readonly return?: string
|
||||
}
|
||||
|
||||
type ForeignFunction = (...args: ReadonlyArray<unknown>) => number | bigint
|
||||
|
||||
export function dlopen(
|
||||
path: string,
|
||||
definitions: Readonly<Record<string, Signature>>,
|
||||
): {
|
||||
readonly lib: { close(): void }
|
||||
readonly functions: Readonly<Record<string, ForeignFunction>>
|
||||
}
|
||||
|
||||
export function getInt32(pointer: number | bigint, offset?: number): number
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { filesystem } from "./effect/app-node-platform"
|
|||
import { LayerNode } from "./effect/layer-node"
|
||||
import { makeRuntime } from "./effect/runtime"
|
||||
import { NpmConfig } from "./npm-config"
|
||||
import { resolveModule } from "#runtime-import"
|
||||
|
||||
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
|
||||
add: Schema.Array(Schema.String).pipe(Schema.optional),
|
||||
|
|
@ -54,9 +55,7 @@ const resolveEntryPoint = (name: string, dir: string, subpaths: readonly string[
|
|||
const entrypoint = subpaths
|
||||
.map((subpath) => {
|
||||
try {
|
||||
return typeof Bun !== "undefined"
|
||||
? import.meta.resolve([name, subpath].filter(Boolean).join("/"), dir)
|
||||
: import.meta.resolve(dir)
|
||||
return resolveModule([name, subpath].filter(Boolean).join("/"), dir)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Effect } from "effect"
|
|||
import { pathToFileURL } from "url"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Npm } from "../../npm"
|
||||
import { importModule } from "#runtime-import"
|
||||
|
||||
export const DynamicProviderPlugin = define({
|
||||
id: "opencode.provider.dynamic",
|
||||
|
|
@ -17,11 +18,9 @@ export const DynamicProviderPlugin = define({
|
|||
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||
|
||||
const mod = yield* Effect.promise(async () => {
|
||||
return (await import(
|
||||
installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
|
||||
)) as Record<string, (options: any) => any>
|
||||
}).pipe(Effect.orDie)
|
||||
const mod = (yield* Effect.promise(() =>
|
||||
importModule(installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href),
|
||||
).pipe(Effect.orDie)) as Record<string, (options: any) => any>
|
||||
const match = Object.keys(mod).find((name) => name.startsWith("create"))
|
||||
if (!match) throw new Error(`Package ${evt.package} has no provider factory export`)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { pathToFileURL } from "url"
|
|||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Npm } from "../../npm"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { importModule } from "#runtime-import"
|
||||
|
||||
export const SapAICorePlugin = define({
|
||||
id: "opencode.provider.sap-ai-core",
|
||||
|
|
@ -22,9 +23,9 @@ export const SapAICorePlugin = define({
|
|||
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||
if (!installedPath) return yield* Effect.die(new Error(`Package ${evt.package} has no import entrypoint`))
|
||||
|
||||
const mod: Record<string, unknown> = yield* Effect.promise(
|
||||
() => import(installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href),
|
||||
)
|
||||
const mod = (yield* Effect.promise(() =>
|
||||
importModule(installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href),
|
||||
)) as Record<string, unknown>
|
||||
const match = Object.keys(mod).find((name) => name.startsWith("create"))
|
||||
if (!match) return yield* Effect.die(new Error(`Package ${evt.package} has no provider factory export`))
|
||||
const factory = mod[match]
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { WellKnown } from "../wellknown"
|
|||
import { PluginInternal } from "./internal"
|
||||
import { PluginRuntime } from "./runtime"
|
||||
import { SdkPlugins } from "./sdk"
|
||||
import { importModule } from "#runtime-import"
|
||||
|
||||
const PluginModule = Schema.Struct({
|
||||
default: Schema.Union([
|
||||
|
|
@ -164,9 +165,13 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract<Op
|
|||
if (!entrypoint) return
|
||||
// Bun currently ignores query parameters when caching file:// imports.
|
||||
const source =
|
||||
operation.mtime === undefined ? entrypoint : `${operation.target.replaceAll("\\", "/")}?mtime=${operation.mtime}`
|
||||
operation.mtime === undefined
|
||||
? entrypoint
|
||||
: typeof Bun !== "undefined"
|
||||
? `${operation.target.replaceAll("\\", "/")}?mtime=${operation.mtime}`
|
||||
: `${entrypoint}?mtime=${operation.mtime}`
|
||||
yield* Effect.log({ msg: "loading plugin", id: operation.target, entrypoint: source })
|
||||
const mod = yield* Effect.promise(() => import(source))
|
||||
const mod = yield* Effect.promise(() => importModule(source))
|
||||
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
||||
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
export * as ProviderV2 from "./provider"
|
||||
|
||||
import { Effect, Schema } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import type { ProviderPackageDefinition } from "@opencode-ai/ai"
|
||||
import { Npm } from "./npm"
|
||||
import type { DeepMutable } from "./schema"
|
||||
import { importModule, resolveModule } from "#runtime-import"
|
||||
|
||||
export const ID = Provider.ID
|
||||
export type ID = typeof ID.Type
|
||||
|
|
@ -32,8 +32,24 @@ export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderV2.
|
|||
export type ProviderPackage = ProviderPackageDefinition
|
||||
|
||||
const packages = new Map<string, Promise<unknown>>()
|
||||
const builtins = new Map<string, () => Promise<unknown>>([
|
||||
["@opencode-ai/ai/providers/amazon-bedrock", () => import("@opencode-ai/ai/providers/amazon-bedrock")],
|
||||
["@opencode-ai/ai/providers/anthropic", () => import("@opencode-ai/ai/providers/anthropic")],
|
||||
["@opencode-ai/ai/providers/azure", () => import("@opencode-ai/ai/providers/azure")],
|
||||
["@opencode-ai/ai/providers/azure/chat", () => import("@opencode-ai/ai/providers/azure/chat")],
|
||||
["@opencode-ai/ai/providers/azure/responses", () => import("@opencode-ai/ai/providers/azure/responses")],
|
||||
["@opencode-ai/ai/providers/google", () => import("@opencode-ai/ai/providers/google")],
|
||||
["@opencode-ai/ai/providers/openai", () => import("@opencode-ai/ai/providers/openai")],
|
||||
["@opencode-ai/ai/providers/openai/chat", () => import("@opencode-ai/ai/providers/openai/chat")],
|
||||
["@opencode-ai/ai/providers/openai/responses", () => import("@opencode-ai/ai/providers/openai/responses")],
|
||||
["@opencode-ai/ai/providers/openai-compatible", () => import("@opencode-ai/ai/providers/openai-compatible")],
|
||||
["@opencode-ai/ai/providers/openrouter", () => import("@opencode-ai/ai/providers/openrouter")],
|
||||
["@opencode-ai/ai/providers/xai", () => import("@opencode-ai/ai/providers/xai")],
|
||||
])
|
||||
|
||||
export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) {
|
||||
const builtin = builtins.get(specifier)
|
||||
if (builtin) return yield* importPackage(specifier, specifier, builtin)
|
||||
const resolved = yield* Effect.sync(() => {
|
||||
if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/ai/")) return specifier
|
||||
try {
|
||||
|
|
@ -53,7 +69,8 @@ export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specif
|
|||
const root = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? specifier)
|
||||
const installed = yield* npm.add(root).pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause })))
|
||||
const entrypoint = yield* Effect.try({
|
||||
try: () => import.meta.resolve(specifier, pathToFileURL(`${installed.directory}/`).href),
|
||||
try: () =>
|
||||
specifier === root && installed.entrypoint ? installed.entrypoint : resolveModule(specifier, installed.directory),
|
||||
catch: (cause) => new LoadError({ package: specifier, cause }),
|
||||
})
|
||||
return yield* importPackage(specifier, entrypoint)
|
||||
|
|
@ -116,12 +133,16 @@ export type Info = Provider.Info
|
|||
|
||||
export type MutableInfo = DeepMutable<Info>
|
||||
|
||||
const importPackage = Effect.fn("ProviderV2.importPackage")(function* (specifier: string, entrypoint: string) {
|
||||
const importPackage = Effect.fn("ProviderV2.importPackage")(function* (
|
||||
specifier: string,
|
||||
entrypoint: string,
|
||||
load = () => importModule(entrypoint),
|
||||
) {
|
||||
const module = yield* Effect.tryPromise({
|
||||
try: () => {
|
||||
const existing = packages.get(entrypoint)
|
||||
if (existing) return existing
|
||||
const loaded = import(entrypoint)
|
||||
const loaded = load()
|
||||
packages.set(entrypoint, loaded)
|
||||
return loaded
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
// ast-grep-ignore: no-star-import
|
||||
import * as pty from "@lydell/node-pty"
|
||||
import { createRequire } from "node:module"
|
||||
import { isSea } from "node:sea"
|
||||
import type { Opts, Proc } from "./pty"
|
||||
|
||||
export type { Disp, Exit, Opts, Proc } from "./pty"
|
||||
|
||||
const pty = createRequire(import.meta.url)(
|
||||
process.env.OPENCODE_NODE_PTY_PATH ?? "@lydell/node-pty",
|
||||
) as typeof import("@lydell/node-pty")
|
||||
|
||||
export function spawn(file: string, args: string[], opts: Opts): Proc {
|
||||
const proc = pty.spawn(file, args, opts)
|
||||
const proc = pty.spawn(file, args, process.platform === "win32" && isSea() ? { ...opts, useConptyDll: true } : opts)
|
||||
return {
|
||||
pid: proc.pid,
|
||||
onData(listener) {
|
||||
|
|
|
|||
7
packages/core/src/runtime/import.bun.ts
Normal file
7
packages/core/src/runtime/import.bun.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export function importModule(specifier: string) {
|
||||
return import(specifier) as Promise<unknown>
|
||||
}
|
||||
|
||||
export function resolveModule(specifier: string, directory: string) {
|
||||
return import.meta.resolve(specifier, directory)
|
||||
}
|
||||
39
packages/core/src/runtime/import.node.ts
Normal file
39
packages/core/src/runtime/import.node.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { Script, constants } from "node:vm"
|
||||
import { createRequire, registerHooks } from "node:module"
|
||||
import path from "node:path"
|
||||
import { pathToFileURL } from "node:url"
|
||||
import { resolve, type Package } from "resolve.exports"
|
||||
|
||||
let conditions: readonly string[] = []
|
||||
const conditionHooks = registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
conditions = context.conditions
|
||||
return nextResolve(specifier, context)
|
||||
},
|
||||
})
|
||||
await new Script('import("node:module")', {
|
||||
importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
||||
}).runInThisContext()
|
||||
conditionHooks.deregister()
|
||||
|
||||
export async function importModule(specifier: string) {
|
||||
const imported = (await new Script(`import(${JSON.stringify(specifier)})`, {
|
||||
importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
||||
}).runInThisContext()) as unknown
|
||||
if (typeof imported !== "object" || imported === null) return imported
|
||||
|
||||
const module = imported as Record<string, unknown>
|
||||
const exports = module["module.exports"]
|
||||
if (exports !== module.default || (typeof exports !== "object" && typeof exports !== "function") || exports === null)
|
||||
return imported
|
||||
return Object.assign({}, module, exports)
|
||||
}
|
||||
|
||||
export function resolveModule(specifier: string, directory: string) {
|
||||
const pkg = createRequire(import.meta.url)(path.join(directory, "package.json")) as Package
|
||||
const target = resolve(pkg, specifier, { conditions, unsafe: true })?.[0]
|
||||
if (target) return pathToFileURL(path.resolve(directory, target)).href
|
||||
const legacyTarget =
|
||||
specifier === pkg.name ? directory : path.resolve(directory, specifier.slice(pkg.name.length + 1))
|
||||
return pathToFileURL(createRequire(path.join(directory, "package.json")).resolve(legacyTarget)).href
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { Global } from "../global"
|
|||
import { makeGlobalNode } from "../effect/app-node"
|
||||
import { httpClient } from "../effect/app-node-platform"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import { Hash } from "../util/hash"
|
||||
|
||||
const skillConcurrency = 4
|
||||
const fileConcurrency = 8
|
||||
|
|
@ -110,7 +111,7 @@ const layer = Layer.effect(
|
|||
)
|
||||
if (!data) return []
|
||||
|
||||
const sourceRoot = path.resolve(global.cache, "skills", Bun.hash(base).toString(16))
|
||||
const sourceRoot = path.resolve(global.cache, "skills", Hash.fast(base))
|
||||
return yield* Effect.forEach(
|
||||
data.skills.flatMap((skill) => {
|
||||
if (!isSafeSegment(skill.name)) {
|
||||
|
|
|
|||
50
packages/core/src/util/process-lock-ffi.bun.ts
Normal file
50
packages/core/src/util/process-lock-ffi.bun.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { dlopen, read, type Pointer } from "bun:ffi"
|
||||
import { existsSync } from "node:fs"
|
||||
|
||||
export type LockResult =
|
||||
| { readonly acquired: true }
|
||||
| { readonly acquired: false; readonly held: true }
|
||||
| { readonly acquired: false; readonly held: false; readonly code: number }
|
||||
|
||||
const LOCK_EX = 2
|
||||
const LOCK_NB = 4
|
||||
const DARWIN_EWOULDBLOCK = 35
|
||||
const LINUX_EWOULDBLOCK = 11
|
||||
|
||||
export function lockDarwin(fd: number): LockResult {
|
||||
const library = dlopen("/usr/lib/libSystem.B.dylib", {
|
||||
flock: { args: ["i32", "i32"], returns: "i32" },
|
||||
__error: { args: [], returns: "ptr" },
|
||||
})
|
||||
try {
|
||||
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
|
||||
const code = result === 0 ? 0 : errorCode(library.symbols.__error())
|
||||
if (result === 0) return { acquired: true }
|
||||
if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true }
|
||||
return { acquired: false, held: false, code }
|
||||
} finally {
|
||||
library.close()
|
||||
}
|
||||
}
|
||||
|
||||
export function lockLinux(fd: number): LockResult {
|
||||
const musl = `/lib/libc.musl-${process.arch === "arm64" ? "aarch64" : "x86_64"}.so.1`
|
||||
const library = dlopen(existsSync(musl) ? musl : "libc.so.6", {
|
||||
flock: { args: ["i32", "i32"], returns: "i32" },
|
||||
__errno_location: { args: [], returns: "ptr" },
|
||||
})
|
||||
try {
|
||||
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
|
||||
const code = result === 0 ? 0 : errorCode(library.symbols.__errno_location())
|
||||
if (result === 0) return { acquired: true }
|
||||
if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true }
|
||||
return { acquired: false, held: false, code }
|
||||
} finally {
|
||||
library.close()
|
||||
}
|
||||
}
|
||||
|
||||
function errorCode(pointer: Pointer | null) {
|
||||
if (pointer === null) throw new Error("Failed to read process lock error code")
|
||||
return read.i32(pointer, 0)
|
||||
}
|
||||
43
packages/core/src/util/process-lock-ffi.node.ts
Normal file
43
packages/core/src/util/process-lock-ffi.node.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { dlopen, getInt32 } from "node:ffi"
|
||||
|
||||
export type LockResult =
|
||||
| { readonly acquired: true }
|
||||
| { readonly acquired: false; readonly held: true }
|
||||
| { readonly acquired: false; readonly held: false; readonly code: number }
|
||||
|
||||
const LOCK_EX = 2
|
||||
const LOCK_NB = 4
|
||||
const DARWIN_EWOULDBLOCK = 35
|
||||
const LINUX_EWOULDBLOCK = 11
|
||||
|
||||
export function lockDarwin(fd: number): LockResult {
|
||||
const library = dlopen("/usr/lib/libSystem.B.dylib", {
|
||||
flock: { arguments: ["int32", "int32"], return: "int32" },
|
||||
__error: { arguments: [], return: "pointer" },
|
||||
})
|
||||
try {
|
||||
const result = library.functions.flock(fd, LOCK_EX | LOCK_NB)
|
||||
const code = result === 0 ? 0 : getInt32(library.functions.__error(), 0)
|
||||
if (result === 0) return { acquired: true }
|
||||
if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true }
|
||||
return { acquired: false, held: false, code }
|
||||
} finally {
|
||||
library.lib.close()
|
||||
}
|
||||
}
|
||||
|
||||
export function lockLinux(fd: number): LockResult {
|
||||
const library = dlopen("libc.so.6", {
|
||||
flock: { arguments: ["int32", "int32"], return: "int32" },
|
||||
__errno_location: { arguments: [], return: "pointer" },
|
||||
})
|
||||
try {
|
||||
const result = library.functions.flock(fd, LOCK_EX | LOCK_NB)
|
||||
const code = result === 0 ? 0 : getInt32(library.functions.__errno_location(), 0)
|
||||
if (result === 0) return { acquired: true }
|
||||
if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true }
|
||||
return { acquired: false, held: false, code }
|
||||
} finally {
|
||||
library.lib.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { dlopen, read, type Pointer } from "bun:ffi"
|
||||
import { closeSync, existsSync, mkdirSync, openSync } from "node:fs"
|
||||
import { lockDarwin, lockLinux, type LockResult } from "#process-lock-ffi"
|
||||
import { closeSync, mkdirSync, openSync } from "node:fs"
|
||||
import { connect, createServer, type Server, type Socket } from "node:net"
|
||||
import path from "node:path"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
|
@ -76,60 +76,12 @@ export namespace ProcessLock {
|
|||
})
|
||||
}
|
||||
|
||||
type Result =
|
||||
| { readonly acquired: true }
|
||||
| { readonly acquired: false; readonly held: true }
|
||||
| { readonly acquired: false; readonly held: false; readonly code: number }
|
||||
|
||||
const LOCK_EX = 2
|
||||
const LOCK_NB = 4
|
||||
const DARWIN_EWOULDBLOCK = 35
|
||||
const LINUX_EWOULDBLOCK = 11
|
||||
|
||||
function lock(fd: number): Result {
|
||||
function lock(fd: number): LockResult {
|
||||
if (process.platform === "darwin") return lockDarwin(fd)
|
||||
if (process.platform === "linux") return lockLinux(fd)
|
||||
throw new Error(`Unsupported process lock platform: ${process.platform}`)
|
||||
}
|
||||
|
||||
function lockDarwin(fd: number): Result {
|
||||
const library = dlopen("/usr/lib/libSystem.B.dylib", {
|
||||
flock: { args: ["i32", "i32"], returns: "i32" },
|
||||
__error: { args: [], returns: "ptr" },
|
||||
})
|
||||
try {
|
||||
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
|
||||
const code = result === 0 ? 0 : errorCode(library.symbols.__error())
|
||||
if (result === 0) return { acquired: true }
|
||||
if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true }
|
||||
return { acquired: false, held: false, code }
|
||||
} finally {
|
||||
library.close()
|
||||
}
|
||||
}
|
||||
|
||||
function lockLinux(fd: number): Result {
|
||||
const musl = `/lib/libc.musl-${process.arch === "arm64" ? "aarch64" : "x86_64"}.so.1`
|
||||
const library = dlopen(existsSync(musl) ? musl : "libc.so.6", {
|
||||
flock: { args: ["i32", "i32"], returns: "i32" },
|
||||
__errno_location: { args: [], returns: "ptr" },
|
||||
})
|
||||
try {
|
||||
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
|
||||
const code = result === 0 ? 0 : errorCode(library.symbols.__errno_location())
|
||||
if (result === 0) return { acquired: true }
|
||||
if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true }
|
||||
return { acquired: false, held: false, code }
|
||||
} finally {
|
||||
library.close()
|
||||
}
|
||||
}
|
||||
|
||||
function errorCode(pointer: Pointer | null) {
|
||||
if (pointer === null) throw new Error("Failed to read process lock error code")
|
||||
return read.i32(pointer, 0)
|
||||
}
|
||||
|
||||
function acquireWindows(file: string) {
|
||||
return Effect.callback<Server, ProcessLock.LockError>((resume) => {
|
||||
const server = createServer()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue