Skip to content
This repository has been archived by the owner on Apr 13, 2024. It is now read-only.

WIP: Add a basic PathCommandProvider #26

Merged
merged 2 commits into from
Apr 10, 2024

Conversation

AtkinsSJ
Copy link
Contributor

This checks for the given command name in each directory in $PATH, and returns an object for executing the first match it finds.

This executing is very basic: the command is run, and the arguments are passed to it, and we wait for it to exit before continuing. But we don't connect stdin, stdout, or stderr. (Because I couldn't figure out how to make a Node stream that works. 😅)

Slight progress towards #14.

@KernelDeimos
Copy link
Collaborator

This is my attempt so far to connect the streams. Unfortunately this is more complex than a call to .pipe() because of how node.js streams and the Streams API differ.

My attempt fails as soon as ctx.externs.out.write(chunk) is called on the callback passed to out.on('data', ...); the error says the stream is closed, but that shouldn't happen because I didn't see the output from console.log('DONE WAITING') (or even the output from console.log('WAITING')).

at the end of execute(ctx), after changing spawnSync to spawn;

const out = new stream.PassThrough();
const err = new stream.PassThrough();
const in_ = new stream.PassThrough();

out.on('data', (chunk) => {
    ctx.externs.out.write(chunk);
});
err.on('data', (chunk) => {
    ctx.externs.err.write(chunk);
});

const fn_err = label => err => {
    console.log(`ERR(${label})`, err);
};
out.on('error', fn_err('out'));
err.on('error', fn_err('err'));
child.stdout.on('error', fn_err('stdout'));
child.stderr.on('error', fn_err('stderr'));
child.stdin.on('error', fn_err('stdin'));


// pipe streams to child process
// out.pipe(child.stdin);
child.stdout.pipe(out);
child.stderr.pipe(err);

let rslv_sigint;
const p_int = new Promise(rslv => rslv_sigint = rslv);
ctx.externs.sig.on((signal) => {
    if ( signal === signals.SIGINT ) {
        rslv_sigint({ is_sigint: true });
    }
});

let data, done;
const next_data = async () => {
    let is_sigint = false;
    ({ value: data, done, is_sigint } = await Promise.race([
        p_int, in_.read(),
    ]));
    if ( is_sigint ) {
        console.log('SIGINT HAPPENED HERE');
        throw new Exit(130);
    }
    // ({ value: line, done } = await in_.read());
}

for ( await next_data() ; ! done ; await next_data() ) {
    console.log('WRITING SOMETHING')
    child.stdin.write(data);
}

// wait for the child to exit
console.log('WAITING');
await new Promise((resolve, reject) => {
    child.on('exit', (code) => {
        ctx.externs.out.write(`Exited with code ${code}`);
        if (code === 0) {
            resolve();
        } else {
            reject(new Error(`Exited with code ${code}`));
        }
    });
});
console.log('DONE WAITING');

@AtkinsSJ AtkinsSJ force-pushed the path-command-provider branch 2 times, most recently from 512a389 to 9a11569 Compare February 22, 2024 17:27
@AtkinsSJ
Copy link
Contributor Author

It complaining that the WritableStream is closed is a complete mystery to me. I bumped into that when I was first trying to make it work originally.

What confuses me is that passing a Node.js Stream object to stdio: [ ... ] makes it throw an error, saying that methods are missing. I think we do want to use spawnSync() so that the child can block, but that means having to provide the streams here instead of attaching callbacks after.

With some trial and error, I've found that it accepts passing in the process's streams. This isn't perfect, but it's a lot closer than what I had before. I've updated my code to do that (and to shrink the try block as you suggested).

@KernelDeimos
Copy link
Collaborator

I'll leave the PR open in the meantime. This is worth continued investigation because passing the streams on process directly will prevent command substitution and pipes from working here.

@AtkinsSJ AtkinsSJ marked this pull request as draft March 19, 2024 14:39
@AtkinsSJ AtkinsSJ changed the title Add a basic PathCommandProvider WIP: Add a basic PathCommandProvider Mar 19, 2024
@AtkinsSJ
Copy link
Contributor Author

Thought I was getting somewhere with this today, but I'm still a little stumped...

  • If we spawn(), which is async, we close the stream here, because execution leaves the await execute(ctx); on line 287 there.
  • If we spawnSync(), we can't pass the custom Stream objects in stdio: [ here ], unless we construct a file descriptor for them to use. From point 6 at https://nodejs.org/api/child_process.html#optionsstdio :

    The stream must have an underlying descriptor

I tried a hack, of using spawn() and ending the function with a spin-loop until the child process has exited. That works!... unless I include your code for handling stdin. My understanding of async JS needs some work, but it seems like await-ing anything makes us leave the function until the awaited thing completes?

Anyway, this feels promising.

@KernelDeimos
Copy link
Collaborator

Yep, await allows other tasks in a queue (managed but the runtime) to be executed - these can be other functions that finished awaiting something for example

@KernelDeimos
Copy link
Collaborator

Spawn should have an exit event so we can resolve a Promise object when the process is done

@AtkinsSJ
Copy link
Contributor Author

Some progress, finally! It's still not perfect, as noted, but it's more correct than it was.

  • stdin handling is wrong.
  • The rejected-promise hack, which isn't making sense to me.

Actually testing SIGINT handling is also impossible until #65 is fixed.

@AtkinsSJ
Copy link
Contributor Author

Another update, to make the code compatible with the upcoming which command, and make it a bit easier to follow.

This checks for the given command name in each directory in $PATH, and
returns an object for executing the first match it finds.

In situations where we don't need to redirect output, we use node-pty to
spawn a new PTY for the command to run in. This works better than our
own ioctl implementation for now, but but can't be used always because
it doesn't allow distinguishing between stderr and stdout. So when we
do need to redirect those, we use Node's process spawning.

Current issues:

- The unhandledRejection callback is very dubious.
- We eat one chunk of stdin input after the executable stops, because we
  can't cancel `ctx.externs.in_.read()`. Possibly this should go via
  another stream that we can disconnect.
- Stdin is always echoed even when the command handles it for us. This
  means typing in the `python` repl makes each character appear
  doubled. It's not actually doubled though, it's just a visual error.
@AtkinsSJ AtkinsSJ marked this pull request as ready for review April 2, 2024 11:55
@AtkinsSJ
Copy link
Contributor Author

AtkinsSJ commented Apr 2, 2024

Changes: We now use node-pty where possible (aka, whenever we don't need to redirect outputs), which gives a much better experience, though not 100% there.

Long-term we'd need to properly handle terminal-related ioctls ourself, and stop needing node-pty, but it's nicer for now.

I've un-drafted in case you do feel like this is worth merging Eric, but fair enough if you don't yet.

@KernelDeimos KernelDeimos merged commit 84c2450 into HeyPuter:trunk Apr 10, 2024
3 checks passed
@AtkinsSJ AtkinsSJ deleted the path-command-provider branch April 10, 2024 14:58
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants