fix(e2b): normalize relative executable paths
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { access } from 'node:fs/promises'
|
import { access } from 'node:fs/promises'
|
||||||
import { join } from 'node:path'
|
import { join, posix } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
@@ -40,6 +40,9 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => {
|
|||||||
} as never)
|
} as never)
|
||||||
const ptyFiber = await ctx.plugin(PtyService)
|
const ptyFiber = await ctx.plugin(PtyService)
|
||||||
const subprocessFiber = await ctx.plugin(E2BSubprocessService)
|
const subprocessFiber = await ctx.plugin(E2BSubprocessService)
|
||||||
|
const node = await ctx.subprocess.resolveExecutable('node')
|
||||||
|
const relativeNodePath = posix.relative(ctx.subprocess.cwd, posix.dirname(node)) || '.'
|
||||||
|
await expect(ctx.subprocess.resolveExecutable('node', { PATH: relativeNodePath })).resolves.toBe(node)
|
||||||
const ownerId = SessionId('e2b-pty-env-owner')
|
const ownerId = SessionId('e2b-pty-env-owner')
|
||||||
const owner: Agent = {
|
const owner: Agent = {
|
||||||
id: ownerId,
|
id: ownerId,
|
||||||
|
|||||||
@@ -79,14 +79,14 @@ export class E2BSubprocessService extends SubprocessService {
|
|||||||
const prefix = path === undefined ? '' : `PATH=${quoteE2BShellArg(path)} `
|
const prefix = path === undefined ? '' : `PATH=${quoteE2BShellArg(path)} `
|
||||||
const result = await sandbox.commands.run(
|
const result = await sandbox.commands.run(
|
||||||
`${prefix}command -v -- ${quoteE2BShellArg(command)}`,
|
`${prefix}command -v -- ${quoteE2BShellArg(command)}`,
|
||||||
signalOpts(signal),
|
{ cwd: this.cwd, ...signalOpts(signal) },
|
||||||
)
|
)
|
||||||
signal?.throwIfAborted()
|
signal?.throwIfAborted()
|
||||||
const executable = result.stdout.trim()
|
const executable = result.stdout.trim()
|
||||||
if (!posix.isAbsolute(executable) || executable.includes('\n')) {
|
if (executable.includes('\n') || (!posix.isAbsolute(executable) && !executable.includes('/'))) {
|
||||||
throw new Error(`subprocess-e2b: executable ${JSON.stringify(command)} did not resolve to one absolute path`)
|
throw new Error(`subprocess-e2b: executable ${JSON.stringify(command)} did not resolve to one absolute path`)
|
||||||
}
|
}
|
||||||
return executable
|
return posix.resolve(this.cwd, executable)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ function commandError(exitCode: number): CommandExitError {
|
|||||||
return new CommandExitError({ exitCode, stdout: '', stderr: '', error: `exit ${exitCode}` })
|
return new CommandExitError({ exitCode, stdout: '', stderr: '', error: `exit ${exitCode}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CommandOptions {
|
||||||
|
signal?: AbortSignal
|
||||||
|
cwd?: string
|
||||||
|
}
|
||||||
|
|
||||||
class FakeTerminalCommandHandle {
|
class FakeTerminalCommandHandle {
|
||||||
pid = 123
|
pid = 123
|
||||||
disconnects = 0
|
disconnects = 0
|
||||||
@@ -74,6 +79,7 @@ class FakeTerminalCommandHandle {
|
|||||||
class FakeTerminalSandbox {
|
class FakeTerminalSandbox {
|
||||||
readonly handle = new FakeTerminalCommandHandle()
|
readonly handle = new FakeTerminalCommandHandle()
|
||||||
readonly commands: string[] = []
|
readonly commands: string[] = []
|
||||||
|
readonly commandOptions: CommandOptions[] = []
|
||||||
readonly inputs: Array<{ pid: number; data: Buffer }> = []
|
readonly inputs: Array<{ pid: number; data: Buffer }> = []
|
||||||
readonly removed: string[] = []
|
readonly removed: string[] = []
|
||||||
readonly directories: string[] = []
|
readonly directories: string[] = []
|
||||||
@@ -121,8 +127,9 @@ class FakeTerminalSandbox {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
commands: {
|
commands: {
|
||||||
run: async (command: string, options?: { signal?: AbortSignal }): Promise<CommandResult> => {
|
run: async (command: string, options?: CommandOptions): Promise<CommandResult> => {
|
||||||
this.commands.push(command)
|
this.commands.push(command)
|
||||||
|
if (options !== undefined) this.commandOptions.push(options)
|
||||||
options?.signal?.throwIfAborted()
|
options?.signal?.throwIfAborted()
|
||||||
if (this.commandFailure !== undefined) {
|
if (this.commandFailure !== undefined) {
|
||||||
const error = this.commandFailure
|
const error = this.commandFailure
|
||||||
@@ -525,12 +532,16 @@ describe('E2B subprocess terminal service', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('publishes execution-world coordinates and resolves remote executables', async () => {
|
it('publishes execution-world coordinates and resolves remote executables', async () => {
|
||||||
const { ctx } = await service()
|
const { ctx, fake } = await service()
|
||||||
expect(ctx.subprocess.cwd).toBe('/workspace')
|
expect(ctx.subprocess.cwd).toBe('/workspace')
|
||||||
expect(ctx.subprocess.runtimeRoot).toBe('/workspace/.dsh-e2b')
|
expect(ctx.subprocess.runtimeRoot).toBe('/workspace/.dsh-e2b')
|
||||||
await expect(ctx.subprocess.resolveExecutable('/bin/bash')).resolves.toBe('/bin/bash')
|
await expect(ctx.subprocess.resolveExecutable('/bin/bash')).resolves.toBe('/bin/bash')
|
||||||
await expect(ctx.subprocess.resolveExecutable('node', { PATH: '/custom/bin' }, new AbortController().signal))
|
await expect(ctx.subprocess.resolveExecutable('node', { PATH: '/custom/bin' }, new AbortController().signal))
|
||||||
.resolves.toBe('/usr/bin/node')
|
.resolves.toBe('/usr/bin/node')
|
||||||
|
fake.resolvedExecutable = 'tools/bin/node\n'
|
||||||
|
await expect(ctx.subprocess.resolveExecutable('node', { PATH: 'tools/bin' }))
|
||||||
|
.resolves.toBe('/workspace/tools/bin/node')
|
||||||
|
expect(fake.commandOptions.at(-1)).toMatchObject({ cwd: '/workspace' })
|
||||||
expect((ctx.e2b)).toBeDefined()
|
expect((ctx.e2b)).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -539,7 +550,7 @@ describe('E2B subprocess terminal service', () => {
|
|||||||
await expect(ctx.subprocess.resolveExecutable('')).rejects.toThrow('non-empty')
|
await expect(ctx.subprocess.resolveExecutable('')).rejects.toThrow('non-empty')
|
||||||
await expect(ctx.subprocess.resolveExecutable('node', undefined, AbortSignal.abort(new Error('stop'))))
|
await expect(ctx.subprocess.resolveExecutable('node', undefined, AbortSignal.abort(new Error('stop'))))
|
||||||
.rejects.toThrow('stop')
|
.rejects.toThrow('stop')
|
||||||
fake.resolvedExecutable = 'relative/node\n'
|
fake.resolvedExecutable = 'node\n'
|
||||||
await expect(ctx.subprocess.resolveExecutable('node')).rejects.toThrow('did not resolve')
|
await expect(ctx.subprocess.resolveExecutable('node')).rejects.toThrow('did not resolve')
|
||||||
fake.resolvedExecutable = '/one\n/two\n'
|
fake.resolvedExecutable = '/one\n/two\n'
|
||||||
await expect(ctx.subprocess.resolveExecutable('node')).rejects.toThrow('did not resolve')
|
await expect(ctx.subprocess.resolveExecutable('node')).rejects.toThrow('did not resolve')
|
||||||
|
|||||||
Reference in New Issue
Block a user