mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 02:57:36 +00:00
perf(wsl): read the kernel, login home and platform in one exec (#35070)
* perf(wsl): read the kernel, login home and platform in one exec The WSL connect's first exec now prints the kernel release, $HOME and the marker-backed uname platform probe together, and hands the platform and home to resolveOrcadRemoteContext, which then skips its own uname and $HOME probes: two fewer serial guest round trips per connect. SSH passes neither, so its probes are unchanged. * fix(wsl): accept the folded host facts only in their exact shape Each fact now prints after its own marker, and the output is used only when it has exactly the kernel line, the home line, the blank separator and the platform line. Anything else (stdout noise, a HOME containing a newline) falls back to reading the kernel alone and lets the context probe the platform and home itself, so no truncated path is ever used.
This commit is contained in:
@@ -52,7 +52,9 @@ export async function resolveOrcadRemoteContext<Target extends OrcadHostTarget =
|
||||
target: Target,
|
||||
connection: OrcadHostTransport,
|
||||
signal?: AbortSignal,
|
||||
detectedHost?: RemoteHostPlatform
|
||||
detectedHost?: RemoteHostPlatform,
|
||||
/** A login home the caller already read on this host; only login-home placement uses it. */
|
||||
knownLoginHome?: string
|
||||
): Promise<OrcadRemoteContext<Target>> {
|
||||
const host = detectedHost ?? (await detectRemoteHostPlatform(connection, { signal }))
|
||||
if (!host) {
|
||||
@@ -65,7 +67,8 @@ export async function resolveOrcadRemoteContext<Target extends OrcadHostTarget =
|
||||
const remote = { conn: connection, host, signal }
|
||||
const { orcadHome: remoteHome, userDataDir } = await resolveOrcadMachineHome(
|
||||
remote,
|
||||
!isWslOrcadHostTarget(target)
|
||||
!isWslOrcadHostTarget(target),
|
||||
knownLoginHome
|
||||
)
|
||||
const serverTarget = await resolveOrcadRuntimeTarget({
|
||||
conn: connection,
|
||||
@@ -103,12 +106,13 @@ export async function resolveOrcadRemoteContext<Target extends OrcadHostTarget =
|
||||
/** Resolve per-machine state for shared POSIX homes; Windows and WSL retain login-home placement. */
|
||||
export async function resolveOrcadMachineHome(
|
||||
remote: OrcadRemoteExecTarget,
|
||||
keyByMachine: boolean
|
||||
keyByMachine: boolean,
|
||||
knownLoginHome?: string
|
||||
): Promise<{ loginHome: string; orcadHome: string; userDataDir: string }> {
|
||||
const { host } = remote
|
||||
if (!keyByMachine || isWindowsRemoteHost(host)) {
|
||||
const loginHome = normalizeRemoteHome(
|
||||
await execOrcadRemote(remote, readRemoteHomeCommand(host)),
|
||||
knownLoginHome ?? (await execOrcadRemote(remote, readRemoteHomeCommand(host))),
|
||||
host
|
||||
)
|
||||
if (!validateRemoteHome(loginHome, host)) {
|
||||
|
||||
@@ -282,4 +282,20 @@ describe('orcad remote context', () => {
|
||||
})
|
||||
expect(order).toEqual(['prepare', 'record'])
|
||||
})
|
||||
|
||||
it('uses a WSL caller-known platform and login home instead of probing them', async () => {
|
||||
vi.mocked(detectRemoteHostPlatform).mockClear()
|
||||
const commands: string[] = []
|
||||
mockExec.mockImplementation(async (_conn, command: string) => {
|
||||
commands.push(command)
|
||||
return command.includes('ldd') ? 'ldd (GNU libc) 2.39\n' : '__ORCAD_RECORD_ABSENT__\n'
|
||||
})
|
||||
const wslTarget = { kind: 'wsl', id: 'wsl:u', distro: 'U', label: 'U' } as const
|
||||
|
||||
const context = await resolveOrcadRemoteContext(wslTarget, conn, undefined, linux, '/home/u\n')
|
||||
|
||||
expect(context).toMatchObject({ remoteHome: '/home/u', userDataDir: '/home/u/.orca' })
|
||||
expect(detectRemoteHostPlatform).not.toHaveBeenCalled()
|
||||
expect(commands.some((command) => command.includes('$HOME'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,8 @@ vi.mock('./remote-exec/remote-exec-command', async (importOriginal) => ({
|
||||
execCommand: execCommandMock
|
||||
}))
|
||||
|
||||
const { detectRemoteHostPlatform } = await import('./ssh-remote-platform-detection')
|
||||
const { detectRemoteHostPlatform, remoteHostPlatformFromUnameProbe } =
|
||||
await import('./ssh-remote-platform-detection')
|
||||
|
||||
const conn = {} as SshConnection
|
||||
|
||||
@@ -264,3 +265,15 @@ describe('detectRemoteHostPlatform failure reporting', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('remoteHostPlatformFromUnameProbe', () => {
|
||||
it('reads only marker-backed platform lines, past other output folded into the same exec', () => {
|
||||
expect(
|
||||
remoteHostPlatformFromUnameProbe(
|
||||
'5.15.167.4-microsoft-standard-WSL2\n/home/u\n\n__ORCA_REMOTE_PLATFORM__ Linux aarch64\n'
|
||||
)
|
||||
).toMatchObject({ os: 'linux', arch: 'arm64' })
|
||||
expect(remoteHostPlatformFromUnameProbe('Linux x86_64\n')).toBeNull()
|
||||
expect(remoteHostPlatformFromUnameProbe('\n__ORCA_REMOTE_PLATFORM__ Linux sparc\n')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,6 +14,8 @@ import { getRemoteHostPlatform, type RemoteHostPlatform } from './ssh-remote-pla
|
||||
import { powerShellCommand } from './ssh-remote-powershell'
|
||||
|
||||
const PLATFORM_PROBE_MARKER = '__ORCA_REMOTE_PLATFORM__'
|
||||
/** The probe's marker-backed line starts with this. */
|
||||
export const UNAME_PLATFORM_PROBE_MARKER = `${PLATFORM_PROBE_MARKER} `
|
||||
const MAX_UNAME_FIELD_CHARS = 64
|
||||
const MAX_THROWN_OUTPUT_CHARS = 200
|
||||
const MAX_LOGGED_OUTPUT_CHARS = 1000
|
||||
@@ -24,6 +26,15 @@ type PlatformProbeOutcome =
|
||||
| { kind: 'unparsed'; output: string }
|
||||
| { kind: 'failed'; error: unknown }
|
||||
|
||||
/** The POSIX platform probe, for callers that fold it into a command of their own. */
|
||||
export const UNAME_PLATFORM_PROBE_COMMAND = `printf '\\n%s ' '${PLATFORM_PROBE_MARKER}'; uname -sm`
|
||||
|
||||
/** The platform a probe's output proves; null for unsupported, unparsed or startup-noise output. */
|
||||
export function remoteHostPlatformFromUnameProbe(output: string): RemoteHostPlatform | null {
|
||||
const outcome = parseRemotePlatformOutput(output)
|
||||
return outcome.kind === 'detected' ? getRemoteHostPlatform(outcome.platform) : null
|
||||
}
|
||||
|
||||
export async function detectRemoteHostPlatform(
|
||||
conn: RemoteInstallTransport,
|
||||
options?: { signal?: AbortSignal }
|
||||
@@ -127,10 +138,9 @@ async function detectUnamePlatform(
|
||||
): Promise<PlatformProbeOutcome> {
|
||||
try {
|
||||
// Separate marker from startup output that may omit its trailing newline.
|
||||
const command = `printf '\\n%s ' '${PLATFORM_PROBE_MARKER}'; uname -sm`
|
||||
const output = signal
|
||||
? await execCommand(conn, command, { signal })
|
||||
: await execCommand(conn, command)
|
||||
? await execCommand(conn, UNAME_PLATFORM_PROBE_COMMAND, { signal })
|
||||
: await execCommand(conn, UNAME_PLATFORM_PROBE_COMMAND)
|
||||
return parseRemotePlatformOutput(output)
|
||||
} catch (error) {
|
||||
signal?.throwIfAborted()
|
||||
|
||||
@@ -108,9 +108,9 @@ function transport(target: WslOrcadHostTarget): OrcadHostTransport {
|
||||
|
||||
function launch(kernel = '5.15.167.4-microsoft-standard-WSL2', options = {}) {
|
||||
mocks.exec.mockImplementation(async (_conn: unknown, command: string) => {
|
||||
if (command === 'uname -r') {
|
||||
if (command.includes('uname -r')) {
|
||||
mocks.steps.push('uname')
|
||||
return `${kernel}\n`
|
||||
return `__ORCA_WSL_KERNEL__ ${kernel}\n__ORCA_WSL_HOME__ /home/u\n\n__ORCA_REMOTE_PLATFORM__ Linux x86_64\n`
|
||||
}
|
||||
return '4242\n'
|
||||
})
|
||||
|
||||
@@ -4,10 +4,18 @@ import type { WslDistroHolder } from './wsl-distro-holder'
|
||||
import type { WslOrcadHostTarget } from './wsl-orcad-host-target'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const state: { spawned: string[]; refused: string[]; probeStep: (() => void) | null } = {
|
||||
const state: {
|
||||
spawned: string[]
|
||||
refused: string[]
|
||||
probeStep: (() => void) | null
|
||||
facts: string
|
||||
contextArgs: unknown[]
|
||||
} = {
|
||||
spawned: [],
|
||||
refused: [],
|
||||
probeStep: null
|
||||
probeStep: null,
|
||||
facts: '',
|
||||
contextArgs: []
|
||||
}
|
||||
return state
|
||||
})
|
||||
@@ -16,25 +24,32 @@ const mocks = vi.hoisted(() => {
|
||||
vi.mock('../ssh/remote-exec/remote-exec-command', () => ({
|
||||
execCommand: vi.fn(
|
||||
async (_conn: unknown, command: string, options?: { signal?: AbortSignal }) => {
|
||||
const label =
|
||||
command === 'uname -r' ? command : command.includes('uname -r') ? 'host-facts' : command
|
||||
if (options?.signal?.aborted) {
|
||||
mocks.refused.push(command)
|
||||
mocks.refused.push(label)
|
||||
throw options.signal.reason
|
||||
}
|
||||
mocks.spawned.push(command)
|
||||
return command === 'uname -r' ? '5.15.167.4-microsoft-standard-WSL2\n' : ''
|
||||
mocks.spawned.push(label)
|
||||
if (label === 'uname -r') {
|
||||
return `${mocks.facts.includes('-Microsoft') ? '4.4.0-19041-Microsoft' : '5.15.167.4-microsoft-standard-WSL2'}\n`
|
||||
}
|
||||
return label === 'host-facts' ? mocks.facts : ''
|
||||
}
|
||||
)
|
||||
}))
|
||||
// Like the real context resolver: several probes in series, each passed the caller's signal.
|
||||
// Like the real context resolver given a known platform and home: its remaining probes in series.
|
||||
vi.mock('../managed-server/install/orcad-remote-context', async () => {
|
||||
const { execCommand } = await import('../ssh/remote-exec/remote-exec-command')
|
||||
return {
|
||||
resolveOrcadRemoteContext: async (
|
||||
_target: unknown,
|
||||
conn: OrcadHostTransport,
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
...known: unknown[]
|
||||
) => {
|
||||
for (const command of ['uname -sm', 'echo $HOME', 'ldd --version']) {
|
||||
mocks.contextArgs = known
|
||||
for (const command of ['ldd --version', 'cat orcad-active.json']) {
|
||||
await new Promise<void>((resolve) => {
|
||||
mocks.probeStep = resolve
|
||||
})
|
||||
@@ -45,7 +60,13 @@ vi.mock('../managed-server/install/orcad-remote-context', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
import { holdWslDistroWithContext, isWsl1KernelRelease } from './wsl-orcad-held-context'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import {
|
||||
holdWslDistroWithContext,
|
||||
isWsl1KernelRelease,
|
||||
readWslHostFacts,
|
||||
WSL_HOST_FACTS_COMMAND
|
||||
} from './wsl-orcad-held-context'
|
||||
|
||||
const target: WslOrcadHostTarget = { kind: 'wsl', id: 'wsl:u', distro: 'U', label: 'U' }
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every host call is mocked at its module.
|
||||
@@ -64,10 +85,17 @@ function holderRejectingOn(gate: Promise<void>): WslDistroHolder {
|
||||
}
|
||||
}
|
||||
|
||||
/** What WSL_HOST_FACTS_COMMAND prints. */
|
||||
function facts(kernel: string, home: string, uname: string): string {
|
||||
return `__ORCA_WSL_KERNEL__ ${kernel}\n__ORCA_WSL_HOME__ ${home}\n\n__ORCA_REMOTE_PLATFORM__ ${uname}\n`
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.spawned.length = 0
|
||||
mocks.refused.length = 0
|
||||
mocks.probeStep = null
|
||||
mocks.facts = facts('5.15.167.4-microsoft-standard-WSL2', '/home/u', 'Linux x86_64')
|
||||
mocks.contextArgs = []
|
||||
})
|
||||
|
||||
describe('holdWslDistroWithContext', () => {
|
||||
@@ -90,14 +118,14 @@ describe('holdWslDistroWithContext', () => {
|
||||
const held = holdWslDistroWithContext('U', target, conn, hold, undefined).finally(() => {
|
||||
settled = true
|
||||
})
|
||||
for (let step = 0; step < 3; step++) {
|
||||
for (let step = 0; step < 2; step++) {
|
||||
await vi.waitFor(() => expect(mocks.probeStep).not.toBeNull())
|
||||
const next = mocks.probeStep
|
||||
mocks.probeStep = null
|
||||
next?.()
|
||||
}
|
||||
await vi.waitFor(() =>
|
||||
expect(mocks.spawned).toEqual(['uname -r', 'uname -sm', 'echo $HOME', 'ldd --version'])
|
||||
expect(mocks.spawned).toEqual(['host-facts', 'ldd --version', 'cat orcad-active.json'])
|
||||
)
|
||||
// The deploy awaits this before recovery, which may launch orcad.
|
||||
expect(settled).toBe(false)
|
||||
@@ -124,14 +152,101 @@ describe('holdWslDistroWithContext', () => {
|
||||
)
|
||||
await vi.waitFor(() => expect(mocks.probeStep).not.toBeNull())
|
||||
mocks.probeStep?.()
|
||||
await vi.waitFor(() => expect(mocks.spawned).toEqual(['uname -r', 'uname -sm']))
|
||||
await vi.waitFor(() => expect(mocks.spawned).toEqual(['host-facts', 'ldd --version']))
|
||||
|
||||
failHold()
|
||||
await expect(held).rejects.toThrow('holder exited')
|
||||
mocks.probeStep?.()
|
||||
await vi.waitFor(() => expect(mocks.refused).toEqual(['echo $HOME']))
|
||||
await vi.waitFor(() => expect(mocks.refused).toEqual(['cat orcad-active.json']))
|
||||
|
||||
expect(mocks.spawned).toEqual(['uname -r', 'uname -sm'])
|
||||
expect(mocks.spawned).toEqual(['host-facts', 'ldd --version'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('the folded host facts probe', () => {
|
||||
function heldNow(): WslDistroHolder {
|
||||
return {
|
||||
...holderRejectingOn(Promise.resolve()),
|
||||
ensureHeld: vi.fn(async (distro: string) => ({
|
||||
outcome: 'adopted' as const,
|
||||
record: { version: 1 as const, distro, pid: 9, creationTimeMs: 1, startedAtMs: 1 }
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveWith(facts: string) {
|
||||
mocks.facts = facts
|
||||
const held = holdWslDistroWithContext('U', target, conn, heldNow(), undefined)
|
||||
for (let step = 0; step < 2; step++) {
|
||||
await vi.waitFor(() => expect(mocks.probeStep).not.toBeNull())
|
||||
const next = mocks.probeStep
|
||||
mocks.probeStep = null
|
||||
next?.()
|
||||
}
|
||||
return held
|
||||
}
|
||||
|
||||
it('hands the context the platform and login home it read in the same exec', async () => {
|
||||
await resolveWith(mocks.facts)
|
||||
|
||||
expect(mocks.spawned.filter((label) => label === 'host-facts')).toHaveLength(1)
|
||||
expect(mocks.contextArgs).toEqual([
|
||||
expect.objectContaining({ os: 'linux', arch: 'x64' }),
|
||||
'/home/u'
|
||||
])
|
||||
})
|
||||
|
||||
it('leaves the context to probe what the facts did not prove', async () => {
|
||||
await resolveWith(facts('5.15.167.4-microsoft-standard-WSL2', '', 'Linux sparc'))
|
||||
|
||||
expect(mocks.contextArgs).toEqual([undefined, undefined])
|
||||
expect(mocks.spawned[0]).toBe('host-facts')
|
||||
expect(mocks.spawned).not.toContain('uname -r')
|
||||
})
|
||||
|
||||
it('falls back to the separate probes for a HOME with a newline, never a truncated path', async () => {
|
||||
await resolveWith(facts('5.15.167.4-microsoft-standard-WSL2', '/home/a\nb', 'Linux x86_64'))
|
||||
|
||||
expect(mocks.contextArgs).toEqual([undefined, undefined])
|
||||
expect(mocks.spawned.slice(0, 2)).toEqual(['host-facts', 'uname -r'])
|
||||
})
|
||||
|
||||
it('falls back without throwing when stdout noise precedes the facts', async () => {
|
||||
await resolveWith(
|
||||
`Welcome to Ubuntu\n${facts('5.15.167.4-microsoft-standard-WSL2', '/home/u', 'Linux x86_64')}`
|
||||
)
|
||||
|
||||
expect(mocks.contextArgs).toEqual([undefined, undefined])
|
||||
expect(mocks.spawned.slice(0, 2)).toEqual(['host-facts', 'uname -r'])
|
||||
})
|
||||
|
||||
it('refuses WSL 1 from the kernel line before holding or probing', async () => {
|
||||
mocks.facts = facts('4.4.0-19041-Microsoft', '/home/u', 'Linux x86_64')
|
||||
const holder = heldNow()
|
||||
|
||||
await expect(
|
||||
holdWslDistroWithContext('U', target, conn, holder, undefined)
|
||||
).rejects.toMatchObject({
|
||||
name: 'OrcadHostUnsupportedError'
|
||||
})
|
||||
expect(holder.ensureHeld).not.toHaveBeenCalled()
|
||||
expect(mocks.spawned).toEqual(['host-facts'])
|
||||
})
|
||||
})
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('WSL_HOST_FACTS_COMMAND under a real sh', () => {
|
||||
it('prints exactly the shape readWslHostFacts accepts', async () => {
|
||||
const result = await runProcess({
|
||||
program: 'sh',
|
||||
args: ['-c', WSL_HOST_FACTS_COMMAND],
|
||||
env: { ...process.env, HOME: '/home/orca user' },
|
||||
timeoutMs: 10_000
|
||||
})
|
||||
|
||||
expect(readWslHostFacts(result.stdout)).toMatchObject({
|
||||
kernel: expect.stringMatching(/\S/),
|
||||
loginHome: '/home/orca user'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,50 @@
|
||||
import { resolveOrcadRemoteContext } from '../managed-server/install/orcad-remote-context'
|
||||
import type { OrcadHostTransport } from '../ssh/orcad-host-transport'
|
||||
import { execCommand } from '../ssh/remote-exec/remote-exec-command'
|
||||
import {
|
||||
remoteHostPlatformFromUnameProbe,
|
||||
UNAME_PLATFORM_PROBE_COMMAND,
|
||||
UNAME_PLATFORM_PROBE_MARKER
|
||||
} from '../ssh/ssh-remote-platform-detection'
|
||||
import type { RemoteHostPlatform } from '../ssh/ssh-remote-platform'
|
||||
import type { WslDistroHolder } from './wsl-distro-holder'
|
||||
import { refuseMissingWslDistro, WslDistroVersion1Error } from './wsl-distro-unsupported'
|
||||
import type { WslOrcadHostTarget } from './wsl-orcad-host-target'
|
||||
|
||||
const KERNEL_MARKER = '__ORCA_WSL_KERNEL__ '
|
||||
const HOME_MARKER = '__ORCA_WSL_HOME__ '
|
||||
export const WSL_HOST_FACTS_COMMAND = `printf '${KERNEL_MARKER}%s\\n${HOME_MARKER}%s\\n' "$(uname -r)" "$HOME"; ${UNAME_PLATFORM_PROBE_COMMAND}`
|
||||
|
||||
type WslHostFacts = { kernel: string; loginHome?: string; platform?: RemoteHostPlatform }
|
||||
|
||||
/**
|
||||
* Accepts only the exact shape the facts command prints; anything else (startup noise, a HOME
|
||||
* with a newline) returns null so the caller probes each fact on its own.
|
||||
*/
|
||||
export function readWslHostFacts(output: string): WslHostFacts | null {
|
||||
const lines = output.split('\n')
|
||||
const [kernelLine = '', homeLine = '', gap, platformLine = '', ...rest] = lines
|
||||
if (
|
||||
!kernelLine.startsWith(KERNEL_MARKER) ||
|
||||
!homeLine.startsWith(HOME_MARKER) ||
|
||||
gap !== '' ||
|
||||
!platformLine.startsWith(UNAME_PLATFORM_PROBE_MARKER) ||
|
||||
rest.some((line) => line.trim() !== '')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const loginHome = homeLine.slice(HOME_MARKER.length)
|
||||
return {
|
||||
kernel: kernelLine.slice(KERNEL_MARKER.length).trim(),
|
||||
...(loginHome.trim() ? { loginHome } : {}),
|
||||
...withPlatform(remoteHostPlatformFromUnameProbe(`\n${platformLine}`))
|
||||
}
|
||||
}
|
||||
|
||||
function withPlatform(platform: RemoteHostPlatform | null): { platform?: RemoteHostPlatform } {
|
||||
return platform ? { platform } : {}
|
||||
}
|
||||
|
||||
/** WSL 1 runs no Linux kernel, so the pinned Node and its pty cannot run there. */
|
||||
export function isWsl1KernelRelease(release: string): boolean {
|
||||
return /^\d+\.\d+\.\d+-\d+-Microsoft$/.test(release.trim())
|
||||
@@ -22,8 +62,13 @@ export async function holdWslDistroWithContext(
|
||||
holder: WslDistroHolder,
|
||||
signal: AbortSignal | undefined
|
||||
) {
|
||||
const kernel = await execCommand(conn, 'uname -r', { signal }).catch(refuseMissingWslDistro(name))
|
||||
if (isWsl1KernelRelease(kernel)) {
|
||||
// One exec for the kernel, the login home and the platform the context would probe one by one.
|
||||
const facts = readWslHostFacts(
|
||||
await execCommand(conn, WSL_HOST_FACTS_COMMAND, { signal }).catch(refuseMissingWslDistro(name))
|
||||
) ??
|
||||
// Unrecognized output proves nothing: read the kernel alone, as before, and let the context probe.
|
||||
{ kernel: await execCommand(conn, 'uname -r', { signal }).catch(refuseMissingWslDistro(name)) }
|
||||
if (isWsl1KernelRelease(facts.kernel)) {
|
||||
throw new WslDistroVersion1Error(name)
|
||||
}
|
||||
// Once the hold fails, stop the probes: after dispose they fall back to fresh wsl.exe execs,
|
||||
@@ -40,7 +85,7 @@ export async function holdWslDistroWithContext(
|
||||
probes.abort(error)
|
||||
throw error
|
||||
}),
|
||||
resolveOrcadRemoteContext(target, conn, probes.signal)
|
||||
resolveOrcadRemoteContext(target, conn, probes.signal, facts.platform, facts.loginHome)
|
||||
])
|
||||
return { held, context }
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user