From f244edfac58ae6e0463e255bb7b6b7cf9f34f5ac Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Fri, 3 May 2024 09:17:17 +0000 Subject: [PATCH 01/11] feature-parity with main --- bun.lockb | Bin 21351 -> 21351 bytes flake.nix | 1 + index.d.ts | 7 ++- index.test.ts | 170 +++++++++++++++++++++++++++----------------------- src/lib.rs | 66 +++++++++++++------- 5 files changed, 143 insertions(+), 101 deletions(-) diff --git a/bun.lockb b/bun.lockb index 50a1bf2d3b0915389a0de113421b50669618e70c..9727b69fec18dc8d029401af7f7b91a1fd2562b7 100755 GIT binary patch delta 23 ecmaF9jPdz0#tmN992|^s#(GA2CYyt-MI``dv, envs: Record, dir: string, size: Size, onExit: (err: null | Error, exitCode: number) => void) + constructor(command: string, args: Array, envs: Record, dir: string, size: Size) + start(onExit: (err: null | Error, exitCode: number) => void): Pid resize(size: Size): void } diff --git a/index.test.ts b/index.test.ts index 6ad32e0..66863d4 100644 --- a/index.test.ts +++ b/index.test.ts @@ -3,56 +3,43 @@ import { Pty } from './index'; describe('PTY', () => { const CWD = process.cwd(); + const ENV = process.env as Record; test('spawns and exits', (done) => { const message = 'hello from a pty'; - const pty = new Pty( - '/bin/echo', - [message], - {}, - CWD, - { rows: 24, cols: 80 }, - (err, exitCode) => { - expect(err).toBeNull(); - expect(exitCode).toBe(0); - done(); - }, - ); + const pty = new Pty('echo', [message], ENV, CWD, { rows: 24, cols: 80 }); const readStream = fs.createReadStream('', { fd: pty.fd }); readStream.on('data', (chunk) => { expect(chunk.toString()).toBe(message + '\r\n'); }); + + pty.start((err, exitCode) => { + expect(err).toBeNull(); + expect(exitCode).toBe(0); + done(); + }); }); test('captures an exit code', (done) => { - new Pty( - '/bin/sh', - ['-c', 'exit 17'], - {}, - CWD, - { rows: 24, cols: 80 }, - (err, exitCode) => { - expect(err).toBeNull(); - expect(exitCode).toBe(17); - done(); - }, - ); + const pty = new Pty('sh', ['-c', 'exit 17'], ENV, CWD, { + rows: 24, + cols: 80, + }); + + pty.start((err, exitCode) => { + expect(err).toBeNull(); + expect(exitCode).toBe(17); + done(); + }); }); test('can be written to', (done) => { const message = 'hello cat'; - const pty = new Pty( - '/bin/cat', - [], - {}, - CWD, - { rows: 24, cols: 80 }, - () => {}, - ); + const pty = new Pty('cat', [], ENV, CWD, { rows: 24, cols: 80 }); const readStream = fs.createReadStream('', { fd: pty.fd }); const writeStream = fs.createWriteStream('', { fd: pty.fd }); @@ -62,18 +49,13 @@ describe('PTY', () => { done(); }); + pty.start(() => {}); + writeStream.write(message); }); test('can be resized', (done) => { - const pty = new Pty( - '/bin/sh', - [], - {}, - CWD, - { rows: 24, cols: 80 }, - () => {}, - ); + const pty = new Pty('sh', [], ENV, CWD, { rows: 24, cols: 80 }); const readStream = fs.createReadStream('', { fd: pty.fd }); const writeStream = fs.createWriteStream('', { fd: pty.fd }); @@ -96,49 +78,40 @@ describe('PTY', () => { } }); + pty.start(() => {}); + writeStream.write("stty size; echo 'done1'\n"); }); test('respects working directory', (done) => { - const pty = new Pty( - '/bin/pwd', - [], - {}, - CWD, - { rows: 24, cols: 80 }, - (err, exitCode) => { - expect(err).toBeNull(); - expect(exitCode).toBe(0); - done(); - }, - ); + const pty = new Pty('pwd', [], ENV, CWD, { rows: 24, cols: 80 }); const readStream = fs.createReadStream('', { fd: pty.fd }); readStream.on('data', (chunk) => { expect(chunk.toString()).toBe(`${CWD}\r\n`); }); + + pty.start((err, exitCode) => { + expect(err).toBeNull(); + expect(exitCode).toBe(0); + done(); + }); }); - test.skip('respects env', (done) => { + test('respects env', (done) => { const message = 'hello from env'; let buffer = ''; const pty = new Pty( - '/bin/sh', - ['-c', 'sleep 0.1s && echo $ENV_VARIABLE && exit'], + 'sh', + ['-c', 'echo $ENV_VARIABLE && exit'], { + ...ENV, ENV_VARIABLE: message, }, CWD, { rows: 24, cols: 80 }, - (err, exitCode) => { - expect(err).toBeNull(); - expect(exitCode).toBe(0); - expect(buffer).toBe(message + '\r\n'); - - done(); - }, ); const readStream = fs.createReadStream('', { fd: pty.fd }); @@ -146,19 +119,20 @@ describe('PTY', () => { readStream.on('data', (chunk) => { buffer += chunk.toString(); }); + + pty.start((err, exitCode) => { + expect(err).toBeNull(); + expect(exitCode).toBe(0); + expect(buffer).toBe(message + '\r\n'); + + done(); + }); }); test('works with Bun.read & Bun.write', (done) => { const message = 'hello bun'; - const pty = new Pty( - '/bin/cat', - [], - {}, - CWD, - { rows: 24, cols: 80 }, - () => {}, - ); + const pty = new Pty('cat', [], ENV, CWD, { rows: 24, cols: 80 }); const file = Bun.file(pty.fd); @@ -173,23 +147,63 @@ describe('PTY', () => { read(); + pty.start(() => {}); + Bun.write(pty.fd, message); }); test("doesn't break when executing non-existing binary", (done) => { + const pty = new Pty('/bin/this-does-not-exist', [], ENV, CWD, { + rows: 24, + cols: 80, + }); + try { - new Pty( - '/bin/this-does-not-exist', - [], - {}, - CWD, - { rows: 24, cols: 80 }, - () => {}, - ); + pty.start(() => {}); } catch (e) { expect(e.message).toContain('No such file or directory'); done(); } }); + + test('can start the process at an arbitrary later point in time', (done) => { + const allFinished = { + dataChunk: false, + startResult: false, + startCallback: false, + }; + + function checkDone() { + if (Object.values(allFinished).every((d) => d === true)) { + done(); + } + } + + const pty = new Pty('echo', ['hello world'], ENV, CWD, { + rows: 24, + cols: 80, + }); + + const readStream = fs.createReadStream('', { fd: pty.fd, start: 0 }); + + readStream.on('data', (chunk) => { + expect(chunk.toString()).toBe('hello world\r\n'); + allFinished.dataChunk = true; + checkDone(); + }); + + setTimeout(() => { + const startResult = pty.start((err, exitCode) => { + expect(err).toBeNull(); + expect(exitCode).toBe(0); + allFinished.startCallback = true; + checkDone(); + }); + + expect(startResult.pid).toBeGreaterThan(0); + allFinished.startResult = true; + checkDone(); + }, 100); + }); }); diff --git a/src/lib.rs b/src/lib.rs index a7ff449..6623285 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,9 +22,16 @@ extern crate napi_derive; #[napi] #[allow(dead_code)] struct Pty { - file: File, + cmd: Command, + file_controller: File, + file_user: File, + #[napi(ts_type = "number")] pub fd: c_int, +} + +#[napi(object)] +struct Pid { pub pid: u32, } @@ -83,7 +90,6 @@ impl Pty { envs: HashMap, dir: String, size: Size, - #[napi(ts_arg_type = "(err: null | Error, exitCode: number) => void")] on_exit: JsFunction, ) -> Result { let window_size = Winsize { ws_col: size.cols, @@ -93,7 +99,10 @@ impl Pty { }; let mut cmd = Command::new(command); + cmd.args(args); + cmd.envs(envs); + cmd.current_dir(dir); let pty_pair = openpty(None, Some(&window_size)) .map_err(|err| NAPI_ERROR::new(napi::Status::GenericFailure, err))?; @@ -101,21 +110,43 @@ impl Pty { let fd_controller = pty_pair.controller.as_raw_fd(); let fd_user = pty_pair.user.as_raw_fd(); - if let Ok(mut termios) = termios::tcgetattr(&pty_pair.controller) { + cmd.stdin(unsafe { Stdio::from_raw_fd(fd_user) }); + cmd.stderr(unsafe { Stdio::from_raw_fd(fd_user) }); + cmd.stdout(unsafe { Stdio::from_raw_fd(fd_user) }); + + let file_controller = File::from(pty_pair.controller); + let file_user = File::from(pty_pair.user); + + Ok(Pty { + fd: fd_controller, + cmd, + file_controller, + file_user, + }) + } + + #[napi] + #[allow(dead_code)] + pub fn start( + &mut self, + #[napi(ts_arg_type = "(err: null | Error, exitCode: number) => void")] on_exit: JsFunction, + ) -> Result { + let ts_on_exit: ThreadsafeFunction = on_exit + .create_threadsafe_function(0, |ctx| ctx.env.create_int32(ctx.value).map(|v| vec![v]))?; + + if let Ok(mut termios) = termios::tcgetattr(&self.file_controller) { termios.input_modes.set(InputModes::IUTF8, true); - termios::tcsetattr(&pty_pair.controller, OptionalActions::Now, &termios) + termios::tcsetattr(&self.file_controller, OptionalActions::Now, &termios) .map_err(|err| NAPI_ERROR::new(napi::Status::GenericFailure, err))?; } - cmd.stdin(unsafe { Stdio::from_raw_fd(fd_user) }); - cmd.stderr(unsafe { Stdio::from_raw_fd(fd_user) }); - cmd.stdout(unsafe { Stdio::from_raw_fd(fd_user) }); + let fd_user = self.file_user.as_raw_fd(); + let fd_controller = self.file_controller.as_raw_fd(); - cmd.envs(envs); - cmd.current_dir(dir); + set_nonblocking(fd_controller)?; unsafe { - cmd.pre_exec(move || { + self.cmd.pre_exec(move || { let err = libc::setsid(); if err == -1 { return Err(Error::new(ErrorKind::Other, "Failed to set session id")); @@ -137,20 +168,13 @@ impl Pty { }); } - let ts_on_exit: ThreadsafeFunction = on_exit - .create_threadsafe_function(0, |ctx| ctx.env.create_int32(ctx.value).map(|v| vec![v]))?; - - let mut child = cmd + let mut child = self + .cmd .spawn() .map_err(|err| NAPI_ERROR::new(GenericFailure, err))?; let pid = child.id(); - set_nonblocking(fd_controller)?; - - let file = File::from(pty_pair.controller); - let fd = file.as_raw_fd(); - // We're creating a new thread for every child, this uses a bit more system resources compared // to alternatives (below), trading off simplicity of implementation. // @@ -192,11 +216,11 @@ impl Pty { // Close the fd once we return from `child.wait()`. unsafe { - rustix::io::close(fd); + rustix::io::close(fd_controller); } }); - Ok(Pty { file, fd, pid }) + Ok(Pid { pid }) } #[napi] From 681fdef7d155b0b5ab30991ce53189acc77296ff Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Fri, 3 May 2024 09:24:15 +0000 Subject: [PATCH 02/11] pin bun version --- .github/workflows/CI.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e623fd6..f4067e3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -46,6 +46,8 @@ jobs: - uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.0.26 - name: Setup node uses: actions/setup-node@v4 if: ${{ !matrix.settings.docker }} @@ -109,6 +111,8 @@ jobs: - uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.0.26 - name: Setup node uses: actions/setup-node@v4 with: @@ -140,6 +144,8 @@ jobs: - uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.0.26 - name: Setup node uses: actions/setup-node@v4 with: @@ -167,6 +173,8 @@ jobs: - uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.0.26 - name: Setup node uses: actions/setup-node@v4 with: From 94c3b664458dcc44c34bcb7ee4bc42ba85a775bb Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Fri, 3 May 2024 09:33:51 +0000 Subject: [PATCH 03/11] more bun pinning --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f4067e3..bf34978 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -161,7 +161,7 @@ jobs: run: ls -R . shell: bash - name: Test bindings - run: docker run --rm -v $(pwd):/build -w /build oven/bun:1 bun test + run: docker run --rm -v $(pwd):/build -w /build oven/bun:1.0.26 bun test publish: name: Publish From 29d7da587bc10c0914c724bedea560798d9fd75d Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Fri, 3 May 2024 09:37:48 +0000 Subject: [PATCH 04/11] env doesn't play nice in docker --- index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.test.ts b/index.test.ts index 66863d4..2753dcc 100644 --- a/index.test.ts +++ b/index.test.ts @@ -99,7 +99,7 @@ describe('PTY', () => { }); }); - test('respects env', (done) => { + test.skip('respects env', (done) => { const message = 'hello from env'; let buffer = ''; From a5ff8c31555cade404454b5ca0200b26280903b6 Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Fri, 3 May 2024 09:47:20 +0000 Subject: [PATCH 05/11] bumping actions --- .github/workflows/CI.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bf34978..80b2e39 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -60,7 +60,7 @@ jobs: toolchain: stable targets: ${{ matrix.settings.target }} - name: Cache cargo - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.cargo/registry/index/ @@ -91,7 +91,7 @@ jobs: if: ${{ !matrix.settings.docker }} shell: bash - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.settings.target }} path: ${{ env.APP_NAME }}.*.node @@ -120,7 +120,7 @@ jobs: - name: Install dependencies run: bun install - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-${{ matrix.settings.target }} path: . @@ -153,7 +153,7 @@ jobs: - name: Install dependencies run: bun install - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-${{ matrix.settings.target }} path: . @@ -182,7 +182,7 @@ jobs: - name: Install dependencies run: bun install - name: Download all artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts - name: Move artifacts From 91931abb4bd768784f90792cd359ffd2b8d47c69 Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Fri, 3 May 2024 11:55:55 +0200 Subject: [PATCH 06/11] bumping napi-rs --- bun.lockb | Bin 21351 -> 21351 bytes index.js | 35 +++++++++++++++++++++++++---------- package.json | 2 +- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/bun.lockb b/bun.lockb index 9727b69fec18dc8d029401af7f7b91a1fd2562b7..8cf7ea90359189f8c55c9c243278b8fc7c6cd5ab 100755 GIT binary patch delta 139 zcmV;60CfN7rUB=s0gx^rc`}k5k7ECal!G2Z Date: Fri, 3 May 2024 10:03:21 +0000 Subject: [PATCH 07/11] target for build? --- .github/workflows/CI.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 80b2e39..621dfe0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,8 +29,9 @@ jobs: settings: - host: macos-latest target: x86_64-apple-darwin - build: | - bun run build + build: |- + set -e && + bun run build --target x86_64-apple-darwin && strip -x *.node - host: ubuntu-latest target: x86_64-unknown-linux-gnu From efffdcdc12aabc5bc7dd9d99b020dd05f06b3ff9 Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Fri, 3 May 2024 10:14:27 +0000 Subject: [PATCH 08/11] explicit architecture --- .github/workflows/CI.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 621dfe0..6ed24cc 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,6 +29,7 @@ jobs: settings: - host: macos-latest target: x86_64-apple-darwin + architecture: x64 build: |- set -e && bun run build --target x86_64-apple-darwin && @@ -107,6 +108,7 @@ jobs: settings: - host: macos-latest target: x86_64-apple-darwin + architecture: x64 runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v4 @@ -118,6 +120,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 + architecture: ${{ matrix.settings.architecture }} - name: Install dependencies run: bun install - name: Download artifacts From 19eb7b3ec66c227de085e7d98841f96caf4e9462 Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Fri, 3 May 2024 10:19:22 +0000 Subject: [PATCH 09/11] macos-14 is x86_64, macos-latest is arm --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6ed24cc..9f662c7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -27,7 +27,7 @@ jobs: fail-fast: false matrix: settings: - - host: macos-latest + - host: macos-14-large target: x86_64-apple-darwin architecture: x64 build: |- @@ -106,7 +106,7 @@ jobs: strategy: matrix: settings: - - host: macos-latest + - host: macos-14-large target: x86_64-apple-darwin architecture: x64 runs-on: ${{ matrix.settings.host }} From 0cd7649b7ea5ddb0f53ef6a493aba7b1aa220cc4 Mon Sep 17 00:00:00 2001 From: lhchavez Date: Fri, 3 May 2024 15:19:37 +0000 Subject: [PATCH 10/11] revert the decoupling changes we probably still want them, but let's split out the CI fixes from new functionality. --- index.d.ts | 7 +-- index.test.ts | 168 +++++++++++++++++++++++--------------------------- src/lib.rs | 66 +++++++------------- 3 files changed, 100 insertions(+), 141 deletions(-) diff --git a/index.d.ts b/index.d.ts index 657f9a8..12f8396 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,16 +3,13 @@ /* auto-generated by NAPI-RS */ -export interface Pid { - pid: number -} export interface Size { cols: number rows: number } export class Pty { fd: number - constructor(command: string, args: Array, envs: Record, dir: string, size: Size) - start(onExit: (err: null | Error, exitCode: number) => void): Pid + pid: number + constructor(command: string, args: Array, envs: Record, dir: string, size: Size, onExit: (err: null | Error, exitCode: number) => void) resize(size: Size): void } diff --git a/index.test.ts b/index.test.ts index 2753dcc..6ad32e0 100644 --- a/index.test.ts +++ b/index.test.ts @@ -3,43 +3,56 @@ import { Pty } from './index'; describe('PTY', () => { const CWD = process.cwd(); - const ENV = process.env as Record; test('spawns and exits', (done) => { const message = 'hello from a pty'; - const pty = new Pty('echo', [message], ENV, CWD, { rows: 24, cols: 80 }); + const pty = new Pty( + '/bin/echo', + [message], + {}, + CWD, + { rows: 24, cols: 80 }, + (err, exitCode) => { + expect(err).toBeNull(); + expect(exitCode).toBe(0); + done(); + }, + ); const readStream = fs.createReadStream('', { fd: pty.fd }); readStream.on('data', (chunk) => { expect(chunk.toString()).toBe(message + '\r\n'); }); - - pty.start((err, exitCode) => { - expect(err).toBeNull(); - expect(exitCode).toBe(0); - done(); - }); }); test('captures an exit code', (done) => { - const pty = new Pty('sh', ['-c', 'exit 17'], ENV, CWD, { - rows: 24, - cols: 80, - }); - - pty.start((err, exitCode) => { - expect(err).toBeNull(); - expect(exitCode).toBe(17); - done(); - }); + new Pty( + '/bin/sh', + ['-c', 'exit 17'], + {}, + CWD, + { rows: 24, cols: 80 }, + (err, exitCode) => { + expect(err).toBeNull(); + expect(exitCode).toBe(17); + done(); + }, + ); }); test('can be written to', (done) => { const message = 'hello cat'; - const pty = new Pty('cat', [], ENV, CWD, { rows: 24, cols: 80 }); + const pty = new Pty( + '/bin/cat', + [], + {}, + CWD, + { rows: 24, cols: 80 }, + () => {}, + ); const readStream = fs.createReadStream('', { fd: pty.fd }); const writeStream = fs.createWriteStream('', { fd: pty.fd }); @@ -49,13 +62,18 @@ describe('PTY', () => { done(); }); - pty.start(() => {}); - writeStream.write(message); }); test('can be resized', (done) => { - const pty = new Pty('sh', [], ENV, CWD, { rows: 24, cols: 80 }); + const pty = new Pty( + '/bin/sh', + [], + {}, + CWD, + { rows: 24, cols: 80 }, + () => {}, + ); const readStream = fs.createReadStream('', { fd: pty.fd }); const writeStream = fs.createWriteStream('', { fd: pty.fd }); @@ -78,25 +96,28 @@ describe('PTY', () => { } }); - pty.start(() => {}); - writeStream.write("stty size; echo 'done1'\n"); }); test('respects working directory', (done) => { - const pty = new Pty('pwd', [], ENV, CWD, { rows: 24, cols: 80 }); + const pty = new Pty( + '/bin/pwd', + [], + {}, + CWD, + { rows: 24, cols: 80 }, + (err, exitCode) => { + expect(err).toBeNull(); + expect(exitCode).toBe(0); + done(); + }, + ); const readStream = fs.createReadStream('', { fd: pty.fd }); readStream.on('data', (chunk) => { expect(chunk.toString()).toBe(`${CWD}\r\n`); }); - - pty.start((err, exitCode) => { - expect(err).toBeNull(); - expect(exitCode).toBe(0); - done(); - }); }); test.skip('respects env', (done) => { @@ -104,14 +125,20 @@ describe('PTY', () => { let buffer = ''; const pty = new Pty( - 'sh', - ['-c', 'echo $ENV_VARIABLE && exit'], + '/bin/sh', + ['-c', 'sleep 0.1s && echo $ENV_VARIABLE && exit'], { - ...ENV, ENV_VARIABLE: message, }, CWD, { rows: 24, cols: 80 }, + (err, exitCode) => { + expect(err).toBeNull(); + expect(exitCode).toBe(0); + expect(buffer).toBe(message + '\r\n'); + + done(); + }, ); const readStream = fs.createReadStream('', { fd: pty.fd }); @@ -119,20 +146,19 @@ describe('PTY', () => { readStream.on('data', (chunk) => { buffer += chunk.toString(); }); - - pty.start((err, exitCode) => { - expect(err).toBeNull(); - expect(exitCode).toBe(0); - expect(buffer).toBe(message + '\r\n'); - - done(); - }); }); test('works with Bun.read & Bun.write', (done) => { const message = 'hello bun'; - const pty = new Pty('cat', [], ENV, CWD, { rows: 24, cols: 80 }); + const pty = new Pty( + '/bin/cat', + [], + {}, + CWD, + { rows: 24, cols: 80 }, + () => {}, + ); const file = Bun.file(pty.fd); @@ -147,63 +173,23 @@ describe('PTY', () => { read(); - pty.start(() => {}); - Bun.write(pty.fd, message); }); test("doesn't break when executing non-existing binary", (done) => { - const pty = new Pty('/bin/this-does-not-exist', [], ENV, CWD, { - rows: 24, - cols: 80, - }); - try { - pty.start(() => {}); + new Pty( + '/bin/this-does-not-exist', + [], + {}, + CWD, + { rows: 24, cols: 80 }, + () => {}, + ); } catch (e) { expect(e.message).toContain('No such file or directory'); done(); } }); - - test('can start the process at an arbitrary later point in time', (done) => { - const allFinished = { - dataChunk: false, - startResult: false, - startCallback: false, - }; - - function checkDone() { - if (Object.values(allFinished).every((d) => d === true)) { - done(); - } - } - - const pty = new Pty('echo', ['hello world'], ENV, CWD, { - rows: 24, - cols: 80, - }); - - const readStream = fs.createReadStream('', { fd: pty.fd, start: 0 }); - - readStream.on('data', (chunk) => { - expect(chunk.toString()).toBe('hello world\r\n'); - allFinished.dataChunk = true; - checkDone(); - }); - - setTimeout(() => { - const startResult = pty.start((err, exitCode) => { - expect(err).toBeNull(); - expect(exitCode).toBe(0); - allFinished.startCallback = true; - checkDone(); - }); - - expect(startResult.pid).toBeGreaterThan(0); - allFinished.startResult = true; - checkDone(); - }, 100); - }); }); diff --git a/src/lib.rs b/src/lib.rs index 6623285..a7ff449 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,16 +22,9 @@ extern crate napi_derive; #[napi] #[allow(dead_code)] struct Pty { - cmd: Command, - file_controller: File, - file_user: File, - + file: File, #[napi(ts_type = "number")] pub fd: c_int, -} - -#[napi(object)] -struct Pid { pub pid: u32, } @@ -90,6 +83,7 @@ impl Pty { envs: HashMap, dir: String, size: Size, + #[napi(ts_arg_type = "(err: null | Error, exitCode: number) => void")] on_exit: JsFunction, ) -> Result { let window_size = Winsize { ws_col: size.cols, @@ -99,10 +93,7 @@ impl Pty { }; let mut cmd = Command::new(command); - cmd.args(args); - cmd.envs(envs); - cmd.current_dir(dir); let pty_pair = openpty(None, Some(&window_size)) .map_err(|err| NAPI_ERROR::new(napi::Status::GenericFailure, err))?; @@ -110,43 +101,21 @@ impl Pty { let fd_controller = pty_pair.controller.as_raw_fd(); let fd_user = pty_pair.user.as_raw_fd(); - cmd.stdin(unsafe { Stdio::from_raw_fd(fd_user) }); - cmd.stderr(unsafe { Stdio::from_raw_fd(fd_user) }); - cmd.stdout(unsafe { Stdio::from_raw_fd(fd_user) }); - - let file_controller = File::from(pty_pair.controller); - let file_user = File::from(pty_pair.user); - - Ok(Pty { - fd: fd_controller, - cmd, - file_controller, - file_user, - }) - } - - #[napi] - #[allow(dead_code)] - pub fn start( - &mut self, - #[napi(ts_arg_type = "(err: null | Error, exitCode: number) => void")] on_exit: JsFunction, - ) -> Result { - let ts_on_exit: ThreadsafeFunction = on_exit - .create_threadsafe_function(0, |ctx| ctx.env.create_int32(ctx.value).map(|v| vec![v]))?; - - if let Ok(mut termios) = termios::tcgetattr(&self.file_controller) { + if let Ok(mut termios) = termios::tcgetattr(&pty_pair.controller) { termios.input_modes.set(InputModes::IUTF8, true); - termios::tcsetattr(&self.file_controller, OptionalActions::Now, &termios) + termios::tcsetattr(&pty_pair.controller, OptionalActions::Now, &termios) .map_err(|err| NAPI_ERROR::new(napi::Status::GenericFailure, err))?; } - let fd_user = self.file_user.as_raw_fd(); - let fd_controller = self.file_controller.as_raw_fd(); + cmd.stdin(unsafe { Stdio::from_raw_fd(fd_user) }); + cmd.stderr(unsafe { Stdio::from_raw_fd(fd_user) }); + cmd.stdout(unsafe { Stdio::from_raw_fd(fd_user) }); - set_nonblocking(fd_controller)?; + cmd.envs(envs); + cmd.current_dir(dir); unsafe { - self.cmd.pre_exec(move || { + cmd.pre_exec(move || { let err = libc::setsid(); if err == -1 { return Err(Error::new(ErrorKind::Other, "Failed to set session id")); @@ -168,13 +137,20 @@ impl Pty { }); } - let mut child = self - .cmd + let ts_on_exit: ThreadsafeFunction = on_exit + .create_threadsafe_function(0, |ctx| ctx.env.create_int32(ctx.value).map(|v| vec![v]))?; + + let mut child = cmd .spawn() .map_err(|err| NAPI_ERROR::new(GenericFailure, err))?; let pid = child.id(); + set_nonblocking(fd_controller)?; + + let file = File::from(pty_pair.controller); + let fd = file.as_raw_fd(); + // We're creating a new thread for every child, this uses a bit more system resources compared // to alternatives (below), trading off simplicity of implementation. // @@ -216,11 +192,11 @@ impl Pty { // Close the fd once we return from `child.wait()`. unsafe { - rustix::io::close(fd_controller); + rustix::io::close(fd); } }); - Ok(Pid { pid }) + Ok(Pty { file, fd, pid }) } #[napi] From cc3954b018e6607d51adda2c9c65b36773dd77f8 Mon Sep 17 00:00:00 2001 From: lhchavez Date: Fri, 3 May 2024 14:36:26 +0000 Subject: [PATCH 11/11] [dev] Also let Linux users develop --- .envrc | 3 +++ .gitignore | 1 + bun.lockb | Bin 21351 -> 21351 bytes flake.nix | 1 + 4 files changed, 5 insertions(+) diff --git a/.envrc b/.envrc index 3550a30..979cc3e 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,4 @@ +if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4=" +fi use flake diff --git a/.gitignore b/.gitignore index 00ed0f8..bed3986 100644 --- a/.gitignore +++ b/.gitignore @@ -196,3 +196,4 @@ Cargo.lock !.yarn/versions *.node +/.direnv diff --git a/bun.lockb b/bun.lockb index 8cf7ea90359189f8c55c9c243278b8fc7c6cd5ab..941803e574b9368b6dfda0d7ceb522a8727cf41f 100755 GIT binary patch delta 346 zcmW;AD+s~>00v=(VzfEmZ`}kzOpA-!7H2aWZ5oZnmBHfTq6mV~Xfzs)TNBfw=zrzo zxGXWU#5nINh7s;7tG^GlO!WbuIIHR$@r6!JC%_clx=w^|Tr_lI%+YJ=B=|+YrE|hk zTfN3BmODBdyrJ3E+2S2LJ)J#1(CX_P@QJg5&Jkbe40Qrb(H-eT_{POpC&nB-ODDlE V`nJx=UWK1RHJlfVm)>MK{Q)oUT^0ZU delta 346 zcmW;AD+s~>00v=)VzkY-^F0M&n2g5RjM|Jwn?|G2GFV((6hSZ=jf+O()&`bE(f`WF zaY<+-p>f@tM$S8!>%S4&W%UtX=u~u0_{LdP=ZrDVYdSur=+<=t%y8Mz3Gs`orp^V6 zE%gTPSh94s_`q^oXNOO;Iywi8(C+FS@r6!L=Y(&Z^>xk|<9wjwV~XxjC%_DswoZs& VT#a-tqqX