From e250a7bb977e9976961f7e041d2bb9ba7558e412 Mon Sep 17 00:00:00 2001 From: Pavel 'Strajk' Dolecek Date: Wed, 27 Sep 2023 10:03:03 +0200 Subject: [PATCH] feat(js-sdk,py-sdk): Rename rootDir/rootdir to cwd, add cwd option to Session (#165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Novák --- .changeset/sixty-pugs-jog.md | 7 + .github/workflows/js_sdk_compat_tests.yml | 2 + apps/docs/src/app/old-page.mdx | 268 ------------------ apps/docs/src/code/js/agents/clone_repo.js | 2 +- .../src/code/js/agents/install_deps_npm.js | 2 +- .../docs/src/code/python/agents/clone_repo.py | 4 +- apps/docs/src/components/Code.tsx | 2 +- apps/docs/src/utils/useSessions.tsx | 2 +- packages/js-sdk/src/session/index.ts | 42 ++- packages/js-sdk/src/session/process.ts | 4 +- .../js-sdk/src/session/sessionConnection.ts | 4 +- packages/js-sdk/src/session/terminal.ts | 6 +- packages/js-sdk/test/process.test.mjs | 4 +- packages/js-sdk/test/run.mjs | 2 +- packages/js-sdk/test/session.test.mjs | 45 ++- packages/js-sdk/testground/basic.cjs | 2 +- packages/python-sdk/e2b/session/main.py | 11 +- packages/python-sdk/e2b/session/process.py | 21 +- .../e2b/session/session_connection.py | 6 + packages/python-sdk/e2b/session/terminal.py | 9 +- packages/python-sdk/example.py | 8 +- packages/python-sdk/python-sdk-example.ipynb | 10 +- packages/python-sdk/tests/test_process.py | 4 +- packages/python-sdk/tests/test_session.py | 24 ++ pnpm-lock.yaml | 8 +- 25 files changed, 185 insertions(+), 314 deletions(-) create mode 100644 .changeset/sixty-pugs-jog.md delete mode 100644 apps/docs/src/app/old-page.mdx diff --git a/.changeset/sixty-pugs-jog.md b/.changeset/sixty-pugs-jog.md new file mode 100644 index 000000000..ba9348aec --- /dev/null +++ b/.changeset/sixty-pugs-jog.md @@ -0,0 +1,7 @@ +--- +'@e2b/python-sdk': minor +'@e2b/sdk': minor +--- + +Renamed process.start argument rootDir to cwd +Added cwd option to Session creation diff --git a/.github/workflows/js_sdk_compat_tests.yml b/.github/workflows/js_sdk_compat_tests.yml index 8d3a27907..5b35edf75 100644 --- a/.github/workflows/js_sdk_compat_tests.yml +++ b/.github/workflows/js_sdk_compat_tests.yml @@ -30,6 +30,8 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + - name: Setup Node uses: actions/setup-node@v3 with: diff --git a/apps/docs/src/app/old-page.mdx b/apps/docs/src/app/old-page.mdx deleted file mode 100644 index 170628d09..000000000 --- a/apps/docs/src/app/old-page.mdx +++ /dev/null @@ -1,268 +0,0 @@ -## Small example - -A simple agent that can write, run, and fix a Next.js webapp based on the information from the linter and any runtime errors. - - {/* # Get the app's content - content = await s.http.get(url) */} - - -```python -from e2b import Session -import asyncio - -task = "" - -# Create an AI coding agent using OpenAI API - - -async def main(): - # Get cloud environment for your agent - # Can also be one of "Nodejs", "Go", "Bash", "Rust", "Python3", "PHP", "Java", "Perl", "DotNET" - # "Nodejs" will have Nodejs, npm, and yarn installed, "Go" will have Go installed, etc. - s = await Session.create("Nodejs") - - # Start a session and create a connection to it - await s.open() - - # Install Next.js - await s.process.start( - cmd="npm install next", - rootdir="/code", - ) - - # Create a Next.js app - await s.process.start( - cmd="npx create-next-app my-app", - rootdir="/code", - ) - - # Start the Next.js app - proc = await s.process.start( - cmd="npm run dev", - rootdir="/code/my-app", - on_stdout=lambda data: print(data), - on_stderr=lambda data: print(data), - on_exit=lambda: print("Exit"), - ) - - # Wait for the app to start - await asyncio.sleep(5) - - # Get the app's URL - url = s.get_hostname(3000) - - # Get the app's content - content = await s.http.get(url) - - # Fix the app's code - # ... - - # Stop the app - await proc.kill() - - # Close the environment - await s.close() - -# Create a simple -``` - - -{/* # Quickstart */} -{/* You don't need to manage any infrastructure to use E2B. -You can get a cloud environment for every running instance of your AI agent in 2 steps: -1. Install the E2B SDK in your project -2. Use the SDK to get a cloud environment for your agent right from your code */} - - -{/* */} - -# Quickstart - -### Installation - -{/* ```bash {{ language: 'js' }} -npm install @e2b/sdk -``` */} - -```bash {{ language: 'python' }} -pip install e2b -``` - - - -{/* */} -{/* ```js -import { Session } from '@e2b/sdk' - -// Get a cloud environment for your agent -const session = new Session({ - id: 'Nodejs', // -}) -// Start a session and create a connection to it -await session.open() - -// List content of the root directory -const dirBContent = await session.filesystem?.list('/') -console.log(dirBContent) - -await session.close() -``` */} - -{/* ```python -from e2b import Session -import asyncio - -async def main(): - # Get cloud environment for your agent - # Can also be one of "Nodejs", "Go", "Bash", "Rust", "Python3", "PHP", "Java", "Perl", "DotNET" - # "Nodejs" will have Nodejs, npm, and yarn installed, "Go" will have Go installed, etc. - s = await Session.create("Nodejs") - - # List content of the root directory in the cloud environment - root_content = await s.filesystem.list('/') - print(root_content) - - # Run a process in the cloud environment - proc = await session.process.start( - cmd="npx create-react-app my-app", - on_stdout=lambda data: print(data), - on_stderr=lambda data: print(data), - on_exit=lambda: print("Exit"), - rootdir="/code", - ) - - # You can even send data to the process's stdin. - await proc.send_stdin("\n") - - # Close the environment - await s.close() - -asyncio.run(main()) -``` */} - -### Initialize new cloud environment session - -```python -from e2b import Session -# You can use some of the predefined environments by using specific id: -# 'Nodejs', 'Bash', 'Python3', 'Java', 'Go', 'Rust', 'PHP', 'Perl', 'DotNET' -session = Session(id="Nodejs", on_scan_ports=lambda ports: print("Open ports", ports)) - -# Start a session and create a connection to it -await session.open() - -... - -# Close the session after you are done -await session.close() -``` - - -### Use filesystem inside cloud environment - -```python -# List -dir_b_content = await session.filesystem.list("/dirA/dirB") - -# Write -# This will create a new file "file.txt" inside the dir "dirB" with the content "Hello world". -await session.filesystem.write("/dirA/dirB/file.txt", "Hello World") - -# Read -file_content = await session.filesystem.read("/dirA/dirB/file.txt") - -# Remove -# Remove a file. -await session.filesystem.remove("/dirA/dirB/file.txt") -# Remove a directory and all of its content. -await session.filesystem.remove("/dirA") - -# Make dir -# Creates a new directory "dirC" and also "dirA" and "dirB" if those directories don"t already exist. -await session.filesystem.make_dir("/dirA/dirB/dirC") - -# Watch dir for changes -watcher = session.filesystem.watch_dir("/dirA/dirB") -watcher.add_event_listener(lambda e: print("Event", e)) -await watcher.start() -``` - - -### Start process inside cloud environment - -```python -proc = await session.process.start( - cmd="echo Hello World", - on_stdout=on_stdout, - on_stderr=on_stderr, - on_exit=lambda: print("Exit"), - rootdir="/code", -) - -await proc.send_stdin("\n") - -print(proc.process_id) - -await proc.kill() - -# Wait for process to finish -await proc.finished -``` - - -### Create interactive terminal inside cloud environment - -```python -term = await session.terminal.start( - on_data=lambda data: print("Data", data), - on_exit=lambda: print("Exit"), - cols=80, - rows=24, - rootdir="/code", - # If you specify a command, the terminal will be closed after the command finishes. - # cmd="echo Hello World", -) - -await term.send_data("echo 1\n") - -await term.resize(80, 30) - -print(term.terminal_id) - -await term.kill() -``` - - -### Get public hostname for an exposed port inside cloud environment - -```python -# Get hostname for port 3000. The hostname is without the protocol (http://). -hostname = session.get_hostname(3000) -``` - - - - - -Use the Protocol API to access contacts, conversations, group messages, and more and seamlessly integrate your product into the workflows of dozens of devoted Protocol users. {{ className: 'lead' }} - -
-
- -## Getting started {{ anchor: false }} - -To get started, create a new application in your [developer settings](#), then read about how to make requests for the resources you need to access using our HTTP APIs or dedicated client SDKs. When your integration is ready to go live, publish it to our [integrations directory](#) to reach the Protocol community. {{ className: 'lead' }} - -
-
- - - diff --git a/apps/docs/src/code/js/agents/clone_repo.js b/apps/docs/src/code/js/agents/clone_repo.js index e75f7aed7..c7da09e15 100644 --- a/apps/docs/src/code/js/agents/clone_repo.js +++ b/apps/docs/src/code/js/agents/clone_repo.js @@ -24,7 +24,7 @@ console.log(content) console.log('Installing deps...') proc = await session.process.start({ cmd: 'npm install', - rootdir: '/code/open-react-template', + cwd: '/code/open-react-template', onStdout: data => console.log(data.line), onStderr: data => console.log(data.line), }) diff --git a/apps/docs/src/code/js/agents/install_deps_npm.js b/apps/docs/src/code/js/agents/install_deps_npm.js index 95273c5c3..504baa31b 100644 --- a/apps/docs/src/code/js/agents/install_deps_npm.js +++ b/apps/docs/src/code/js/agents/install_deps_npm.js @@ -12,7 +12,7 @@ const session = await Session.create({ console.log('Installing lodash...') const proc = await session.process.start({ cmd: 'npm install lodash', // $HighlightLine - rootdir: '/code', // $HighlightLine + cwd: '/code', // $HighlightLine onStdout: (data) => console.log('[INFO] ', data.line), onStderr: (data) => console.log('[WARN | ERROR] ', data.line), }) diff --git a/apps/docs/src/code/python/agents/clone_repo.py b/apps/docs/src/code/python/agents/clone_repo.py index 0e12bb459..9e5be4e96 100644 --- a/apps/docs/src/code/python/agents/clone_repo.py +++ b/apps/docs/src/code/python/agents/clone_repo.py @@ -34,11 +34,11 @@ async def main(): cmd="npm install", on_stdout=print_out, on_stderr=print_out, - rootdir="/code/open-react-template" + cwd="/code/open-react-template" ) await proc await session.close() -asyncio.run(main()) \ No newline at end of file +asyncio.run(main()) diff --git a/apps/docs/src/components/Code.tsx b/apps/docs/src/components/Code.tsx index b40d7a01d..707a94103 100644 --- a/apps/docs/src/components/Code.tsx +++ b/apps/docs/src/components/Code.tsx @@ -130,7 +130,7 @@ function CodePanel({ await session.filesystem.write(filename, code) session.process.start({ cmd: `${runtime} ${filename}`, - rootdir: '/code', + cwd: '/code', onStdout: appendOutput, onStderr: filterAndMaybeAppendOutput, onExit: () => setIsRunning(false), diff --git a/apps/docs/src/utils/useSessions.tsx b/apps/docs/src/utils/useSessions.tsx index 9d13bb191..71456c986 100644 --- a/apps/docs/src/utils/useSessions.tsx +++ b/apps/docs/src/utils/useSessions.tsx @@ -66,7 +66,7 @@ export const useSessionsStore = create((set, get) => ({ cmd: preps[lang], onStdout: stdHandler, onStderr: stdHandler, - rootdir: '/code', + cwd: '/code', }) await proc.finished // await prep process to finish log(`${lang} session created and started`) diff --git a/packages/js-sdk/src/session/index.ts b/packages/js-sdk/src/session/index.ts index 436102080..0c9f701e5 100644 --- a/packages/js-sdk/src/session/index.ts +++ b/packages/js-sdk/src/session/index.ts @@ -4,8 +4,8 @@ import { components } from '../api' import { id } from '../utils/id' import { createDeferredPromise, formatSettledErrors, withTimeout } from '../utils/promise' import { - ScanOpenedPortsHandler as ScanOpenPortsHandler, codeSnippetService, + ScanOpenedPortsHandler as ScanOpenPortsHandler, } from './codeSnippet' import { FileInfo, FilesystemManager, filesystemService } from './filesystem' import FilesystemWatcher from './filesystemWatcher' @@ -140,7 +140,7 @@ export class Session extends SessionConnection { onExit, envVars, cmd, - rootdir = '', + cwd = '', terminalID = id(12), timeout = undefined, }: TerminalOpts) => { @@ -150,10 +150,18 @@ export class Session extends SessionConnection { onExit, envVars, cmd, - rootdir = '', + cwd = '', + rootDir, terminalID = id(12), }: Omit) => { this.logger.debug?.(`Starting terminal "${terminalID}"`) + if (!cwd && rootDir) { + this.logger.warn?.('The rootDir parameter is deprecated, use cwd instead.') + cwd = rootDir + } + if (!cwd && this.opts.cwd) { + cwd = this.opts.cwd + } const { promise: terminalExited, resolve: triggerExit } = createDeferredPromise() @@ -194,7 +202,7 @@ export class Session extends SessionConnection { size.cols, size.rows, // Handle optional args for old devbookd compatibility - ...(cmd !== undefined ? [envVars, cmd, rootdir] : []), + ...(cmd !== undefined ? [envVars, cmd, cwd] : []), ]) } catch (err) { triggerExit() @@ -213,7 +221,7 @@ export class Session extends SessionConnection { onExit, envVars, cmd, - rootdir, + cwd, terminalID, }) }, @@ -228,13 +236,19 @@ export class Session extends SessionConnection { onStderr, onExit, envVars = {}, - rootdir = '', + cwd = '', + rootDir, processID = id(12), }: Omit) => { - if (!cmd) { - throw new Error('cmd is required') + if (!cwd && rootDir) { + this.logger.warn?.('The rootDir parameter is deprecated, use cwd instead.') + cwd = rootDir + } + if (!cwd && this.opts.cwd) { + cwd = this.opts.cwd } - this.logger.debug?.(`Starting process "${processID}"`) + if (!cmd) throw new Error('cmd is required') + this.logger.debug?.(`Starting process "${processID}", cmd: "${cmd}"`) const { promise: processExited, resolve: triggerExit } = createDeferredPromise() @@ -280,7 +294,7 @@ export class Session extends SessionConnection { }) try { - await this.call(processService, 'start', [processID, cmd, envVars, rootdir]) + await this.call(processService, 'start', [processID, cmd, envVars, cwd]) } catch (err) { triggerExit() await unsubscribing @@ -296,7 +310,13 @@ export class Session extends SessionConnection { } static async create(opts: SessionOpts) { - return new Session(opts).open({ timeout: opts?.timeout }) + return new Session(opts).open({ timeout: opts?.timeout }).then(async session => { + if (opts.cwd) { + console.log(`Custom cwd for Session set: "${opts.cwd}"`) + await session.filesystem.makeDir(opts.cwd) + } + return session + }) } override async open(opts: CallOpts) { diff --git a/packages/js-sdk/src/session/process.ts b/packages/js-sdk/src/session/process.ts index e448ebfbb..7a2ad38c2 100644 --- a/packages/js-sdk/src/session/process.ts +++ b/packages/js-sdk/src/session/process.ts @@ -113,7 +113,9 @@ export interface ProcessOpts { onStderr?: (out: ProcessMessage) => void onExit?: () => void envVars?: EnvVars - rootdir?: string + cwd?: string + /** @deprecated use cwd instead */ + rootDir?: string processID?: string timeout?: number } diff --git a/packages/js-sdk/src/session/sessionConnection.ts b/packages/js-sdk/src/session/sessionConnection.ts index c6ae32f97..1a6c9f975 100644 --- a/packages/js-sdk/src/session/sessionConnection.ts +++ b/packages/js-sdk/src/session/sessionConnection.ts @@ -41,6 +41,7 @@ interface Logger { export interface SessionConnectionOpts { id: string apiKey: string + cwd?: string logger?: Logger __debug_hostname?: string __debug_port?: number @@ -65,7 +66,8 @@ export class SessionConnection { private readonly rpc = new RpcWebSocketClient() private subscribers: Subscriber[] = [] - constructor(private readonly opts: SessionConnectionOpts) { + // let's keep opts readonly, but public – for convenience, mainly when debugging + constructor(readonly opts: SessionConnectionOpts) { if (!opts.apiKey) { throw new AuthenticationError( 'API key is required, please visit https://e2b.dev/docs to get your API key', diff --git a/packages/js-sdk/src/session/terminal.ts b/packages/js-sdk/src/session/terminal.ts index bd69f54b5..07f5eed92 100644 --- a/packages/js-sdk/src/session/terminal.ts +++ b/packages/js-sdk/src/session/terminal.ts @@ -77,7 +77,11 @@ export type TerminalOpts = { /** * Working directory where will the terminal start. */ - rootdir?: string + cwd?: string + /** + * @deprecated use cwd instead + */ + rootDir?: string /** * Environment variables that will be accessible inside of the terminal. */ diff --git a/packages/js-sdk/test/process.test.mjs b/packages/js-sdk/test/process.test.mjs index 62f6d8401..3f7fb666e 100644 --- a/packages/js-sdk/test/process.test.mjs +++ b/packages/js-sdk/test/process.test.mjs @@ -13,7 +13,7 @@ test('process on stdout/stderr', async () => { cmd: 'pwd', onStdout: data => stdout.push(data), onStderr: data => stderr.push(data), - rootdir: '/tmp', + cwd: '/tmp', }) const output = await process.finished @@ -55,7 +55,7 @@ test('process send stdin', async () => { const process = await session.process.start({ cmd: 'read -r line; echo "$line"', - rootdir: '/code', + cwd: '/code', }) await process.sendStdin('ping\n') await process.finished diff --git a/packages/js-sdk/test/run.mjs b/packages/js-sdk/test/run.mjs index 0c2972fe0..963415e1b 100644 --- a/packages/js-sdk/test/run.mjs +++ b/packages/js-sdk/test/run.mjs @@ -40,7 +40,7 @@ raise Exception("err") // cols: 9, // rows: 9, // }, - // rootdir: '/code', + // cwd: '/code', // cmd: 'npm i', // }) diff --git a/packages/js-sdk/test/session.test.mjs b/packages/js-sdk/test/session.test.mjs index aa626384c..a87ca67e1 100644 --- a/packages/js-sdk/test/session.test.mjs +++ b/packages/js-sdk/test/session.test.mjs @@ -1,5 +1,5 @@ import { Session } from '../src' -import { test } from 'vitest' +import { test, expect } from 'vitest' const E2B_API_KEY = process.env.E2B_API_KEY @@ -14,3 +14,46 @@ test('create multiple sessions', async () => { await session.close() await session2.close() }, 10000) + +test( + 'custom cwd', + async () => { + const session = await Session.create({ + id: 'Nodejs', + apiKey: E2B_API_KEY, + cwd: '/code/app', + }) + + { + const proc = await session.process.start({ cmd: 'pwd' }) + await proc.finished + const out = proc.output.stdout + expect(out).toEqual('/code/app') + } + + // filesystem ops does not respect the cwd yet + { + await session.filesystem.write('hello.txt', `Hello VM!`) + const proc = await session.process.start({ cmd: 'cat /hello.txt' }) + await proc.finished + const out = proc.output.stdout + expect(out).toEqual('Hello VM!') + } + + // change dir to /home/user + { + const proc = await session.process.start({ cmd: 'cd /home/user' }) + await proc.finished + } + + // create another file, it should still be in root + { + await session.filesystem.write('hello2.txt', `Hello VM 2!`) + const proc = await session.process.start({ cmd: 'cat /hello2.txt' }) + await proc.finished + const out = proc.output.stdout + expect(out).toEqual('Hello VM 2!') + } + }, + { timeout: 10_000 }, +) diff --git a/packages/js-sdk/testground/basic.cjs b/packages/js-sdk/testground/basic.cjs index 080948ac8..7d14458f6 100644 --- a/packages/js-sdk/testground/basic.cjs +++ b/packages/js-sdk/testground/basic.cjs @@ -18,7 +18,7 @@ async function main() { const proc = await session.process.start({ cmd: 'npm i --silent', envVars: { NPM_CONFIG_UPDATE_NOTIFIER: "false" }, - rootdir: '/code', + cwd: '/code', onStdout: ({ line }) => console.log('STDOUT', line), onStderr: ({ line }) => console.log('STDERR', line), }) diff --git a/packages/python-sdk/e2b/session/main.py b/packages/python-sdk/e2b/session/main.py index 11f2e6f12..66f1d530e 100644 --- a/packages/python-sdk/e2b/session/main.py +++ b/packages/python-sdk/e2b/session/main.py @@ -9,7 +9,6 @@ from e2b.session.session_connection import SessionConnection from e2b.session.terminal import TerminalManager - logger = logging.getLogger(__name__) Environment = Literal[ @@ -63,6 +62,7 @@ def __init__( self, id: Union[Environment, str], api_key: str, + cwd: Optional[str] = None, on_scan_ports: Optional[Callable[[List[OpenPort]], Any]] = None, _debug_hostname: Optional[str] = None, _debug_port: Optional[int] = None, @@ -84,6 +84,7 @@ def __init__( - `DotNET` :param api_key: The API key to use + :param cwd: The current working directory to use :param edit_enabled: Whether the session state will be saved after exit :param on_scan_ports: A callback to handle opened ports """ @@ -92,6 +93,7 @@ def __init__( super().__init__( id=id, api_key=api_key, + cwd=cwd, _debug_hostname=_debug_hostname, _debug_port=_debug_port, _debug_dev_env=_debug_dev_env, @@ -137,6 +139,7 @@ async def create( cls, id: Union[Environment, str], api_key: Optional[str] = None, + cwd: Optional[str] = None, on_scan_ports: Optional[Callable[[List[OpenPort]], Any]] = None, timeout: Optional[float] = TIMEOUT, _debug_hostname: Optional[str] = None, @@ -159,17 +162,21 @@ async def create( - `DotNET` :param api_key: The API key to use + :param cwd: The current working directory to use :param on_scan_ports: A callback to handle opened ports :param timeout: Specify the duration, in seconds to give the method to finish its execution before it times out (default is 60 seconds). If set to None, the method will continue to wait until it completes, regardless of time """ - session = cls( id=id, api_key=api_key, + cwd=cwd, on_scan_ports=on_scan_ports, _debug_hostname=_debug_hostname, _debug_port=_debug_port, _debug_dev_env=_debug_dev_env, ) await session.open(timeout=timeout) + if cwd: + await session.filesystem.make_dir(cwd) + return session diff --git a/packages/python-sdk/e2b/session/process.py b/packages/python-sdk/e2b/session/process.py index 214828111..cf7828d05 100644 --- a/packages/python-sdk/e2b/session/process.py +++ b/packages/python-sdk/e2b/session/process.py @@ -196,7 +196,8 @@ async def start( on_stderr: Optional[Callable[[ProcessMessage], Any]] = None, on_exit: Optional[Callable[[], Any]] = None, env_vars: Optional[EnvVars] = None, - rootdir: str = "", + cwd: str = "", + rootdir: str = "", # DEPRECATED process_id: Optional[str] = None, timeout: Optional[float] = TIMEOUT, ) -> Process: @@ -208,13 +209,16 @@ async def start( :param on_stderr: A callback that is called when stderr with a newline is received from the process :param on_exit: A callback that is called when the process exits :param env_vars: A dictionary of environment variables to set for the process - :param rootdir: The root directory for the process + :param cwd: The root directory for the process + :param rootdir: (DEPRECATED - use cwd) The root directory for the process + .. deprecated:: 0.3.2 + Use cwd instead. :param process_id: The process id to use for the process. If not provided, a random id is generated :param timeout: Specify the duration, in seconds to give the method to finish its execution before it times out (default is 60 seconds). If set to None, the method will continue to wait until it completes, regardless of time :return: A process object """ - logger.info(f"Starting process (id: {process_id})") + logger.info(f"Starting process (id: {process_id}): {cmd}") async with async_timeout.timeout(timeout): if not env_vars: env_vars = {} @@ -308,6 +312,15 @@ async def trigger_exit(): logger.debug(f"Exited the process (id: {process_id})") try: + if not cwd and rootdir: + cwd = rootdir + logger.warning( + "The rootdir parameter is deprecated, use cwd instead." + ) + + if not cwd and self._session.cwd: + cwd = self._session.cwd + await self._session._call( self._service_name, "start", @@ -315,7 +328,7 @@ async def trigger_exit(): process_id, cmd, env_vars, - rootdir, + cwd, ], ) logger.info(f"Started process (id: {process_id})") diff --git a/packages/python-sdk/e2b/session/session_connection.py b/packages/python-sdk/e2b/session/session_connection.py index 93ef2c008..137ecb0e4 100644 --- a/packages/python-sdk/e2b/session/session_connection.py +++ b/packages/python-sdk/e2b/session/session_connection.py @@ -40,6 +40,10 @@ class Subscription(BaseModel): class SessionConnection: _refresh_retries = 4 + @property + def cwd(self): + return self._cwd + @property def finished(self): """ @@ -61,6 +65,7 @@ def __init__( self, id: str, api_key: str, + cwd: Optional[str] = None, _debug_hostname: Optional[str] = None, _debug_port: Optional[int] = None, _debug_dev_env: Optional[Literal["remote", "local"]] = None, @@ -73,6 +78,7 @@ def __init__( self._id = id self._api_key = api_key + self._cwd = cwd self._debug_hostname = _debug_hostname self._debug_port = _debug_port self._debug_dev_env = _debug_dev_env diff --git a/packages/python-sdk/e2b/session/terminal.py b/packages/python-sdk/e2b/session/terminal.py index cfa9457dd..866a3ac7e 100644 --- a/packages/python-sdk/e2b/session/terminal.py +++ b/packages/python-sdk/e2b/session/terminal.py @@ -148,7 +148,7 @@ async def start( on_data: Callable[[str], Any], cols: int, rows: int, - rootdir: str = "", + cwd: str = "", terminal_id: Optional[str] = None, on_exit: Optional[Callable[[], Any]] = None, cmd: Optional[str] = None, @@ -159,7 +159,7 @@ async def start( Start a new terminal session. :param on_data: Callback that will be called when the terminal sends data - :param rootdir: Working directory where will the terminal start + :param cwd: Working directory where will the terminal start :param terminal_id: Unique identifier of the terminal session :param on_exit: Callback that will be called when the terminal exits :param cols: Number of columns the terminal will have. This affects rendering @@ -229,6 +229,9 @@ async def trigger_exit(): await future_exit_handler_finish try: + if not cwd and self._session.cwd: + cwd = self._session.cwd + await self._session._call( self._service_name, "start", @@ -238,7 +241,7 @@ async def trigger_exit(): rows, env_vars if env_vars else {}, cmd, - rootdir, + cwd, ], timeout=timeout, ) diff --git a/packages/python-sdk/example.py b/packages/python-sdk/example.py index bd63c9c4a..0e7a1a2ab 100644 --- a/packages/python-sdk/example.py +++ b/packages/python-sdk/example.py @@ -24,7 +24,7 @@ async def main(): session = await Session.create(id) # proc = await session.terminal.start( - # rootdir="/code", + # cwd="/code", # cols=80, # rows=24, # on_data=lambda data: print("DATA", data), @@ -39,7 +39,7 @@ async def main(): # await proc.finished proc = await session.process.start( "ls", - rootdir="/", + cwd="/", on_stderr=lambda data: print("ERR", data), on_stdout=lambda data: print("OUT", data), ) @@ -89,7 +89,7 @@ def on_stderr(data): on_stdout=on_stdout, on_stderr=on_stderr, on_exit=lambda: print("EXIT"), - rootdir="/code", + cwd="/code", ) # await proc.send_stdin("lore olympus") # await proc.send_stdin("\nnew line too") @@ -104,7 +104,7 @@ def on_stderr(data): on_data=lambda data: print("DATA", data), cols=80, rows=24, - rootdir="/code", + cwd="/code", ) await term.send_data("echo 1\n") diff --git a/packages/python-sdk/python-sdk-example.ipynb b/packages/python-sdk/python-sdk-example.ipynb index 08e422982..b2f6a9910 100644 --- a/packages/python-sdk/python-sdk-example.ipynb +++ b/packages/python-sdk/python-sdk-example.ipynb @@ -260,7 +260,7 @@ " # You can still access the stderr after `output = await proc` as `output.stderr` or anytime as `proc.output.stderr`.\n", " on_stderr=lambda data: print(\"Stderr\", data),\n", " on_exit=lambda: print(\"Exit\"),\n", - " rootdir=\"/code\",\n", + " cwd=\"/code\",\n", ")\n", "print(\"Process session ID\", proc.process_id)\n", "\n", @@ -316,7 +316,7 @@ " # You can still access the stderr after `output = await proc` as `output.stderr` or anytime as `proc.output.stderr`.\n", " on_stderr=lambda data: print(\"Stderr\", data),\n", " on_exit=lambda: print(\"Exit\"),\n", - " rootdir=\"/code\",\n", + " cwd=\"/code\",\n", ")\n", "await proc.send_stdin(\"marco\\n\")\n", "await proc.kill()\n", @@ -366,7 +366,7 @@ " on_exit=lambda: print(\"Exit\"),\n", " cols=80,\n", " rows=24,\n", - " rootdir=\"/code\",\n", + " cwd=\"/code\",\n", ")\n", "print(\"Terminal session ID\", term.terminal_id)\n", "await term.resize(80, 30)\n", @@ -403,7 +403,7 @@ " on_exit=lambda: print(\"Exit\"),\n", " cols=80,\n", " rows=24,\n", - " rootdir=\"/code\",\n", + " cwd=\"/code\",\n", " # If you specify a command, the terminal will be closed after the command finishes.\n", " cmd=\"echo Hello World\",\n", ")\n", @@ -494,7 +494,7 @@ "\n", "proc = await session.process.start(\n", " \"pwd\",\n", - " rootdir=\"/code\",\n", + " cwd=\"/code\",\n", ")\n", "\n", "# You can access `proc.output...` even before the process finishes\n", diff --git a/packages/python-sdk/tests/test_process.py b/packages/python-sdk/tests/test_process.py index 83aa89055..dd3625eb1 100644 --- a/packages/python-sdk/tests/test_process.py +++ b/packages/python-sdk/tests/test_process.py @@ -23,7 +23,7 @@ async def test_process_on_stdout_stderr(): "pwd", on_stdout=lambda data: stdout.append(data), on_stderr=lambda data: stderr.append(data), - rootdir="/tmp", + cwd="/tmp", ) output = await proc @@ -58,7 +58,7 @@ async def test_process_send_stdin(): proc = await session.process.start( 'read -r line; echo "$line"', - rootdir="/code", + cwd="/code", ) await proc.send_stdin("ping\n") await proc diff --git a/packages/python-sdk/tests/test_session.py b/packages/python-sdk/tests/test_session.py index bf4acefb6..b2998d75f 100644 --- a/packages/python-sdk/tests/test_session.py +++ b/packages/python-sdk/tests/test_session.py @@ -15,3 +15,27 @@ async def test_create_multiple_sessions(): session2 = await Session.create("Nodejs", api_key=E2B_API_KEY) await session.close() await session2.close() + + +async def test_custom_cwd(): + session = await Session.create("Nodejs", api_key=E2B_API_KEY, cwd="/code/app") + + proc = await session.process.start("pwd") + output = await proc + assert output.stdout == "/code/app" + + # filesystem ops does not respect the cwd yet + await session.filesystem.write("hello.txt", "Hello VM!") + proc = await session.process.start("cat /hello.txt") # notice the file is in root + output = await proc + assert output.stdout == "Hello VM!" + + # change dir to /home/user + proc = await session.process.start("cd /home/user") + await proc + + # create another file, it should still be in root + await session.filesystem.write("hello2.txt", "Hello VM 2!") + proc = await session.process.start("cat /hello2.txt") + output = await proc + assert output.stdout == "Hello VM 2!" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a35bfa54..0ccbe99bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -347,7 +347,7 @@ importers: version: 18.2.23 '@types/react-dom': specifier: latest - version: 18.2.7 + version: 18.2.8 typescript: specifier: latest version: 5.2.2 @@ -3244,6 +3244,12 @@ packages: dependencies: '@types/react': 18.2.22 + /@types/react-dom@18.2.8: + resolution: {integrity: sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==} + dependencies: + '@types/react': 18.2.23 + dev: true + /@types/react-highlight-words@0.16.4: resolution: {integrity: sha512-KITBX3xzheQLu2s3bUgLmRE7ekmhc52zRjRTwkKayQARh30L4fjEGzGm7ULK9TuX2LgxWWavZqyQGDGjAHbL3w==} dependencies: