-
Notifications
You must be signed in to change notification settings - Fork 272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support all CLI inputs in PHP interactive shell #118
Comments
Passing the following raw bytes to stdin: const stdIn = new Uint8Array([
// send echo "a":
...new TextEncoder().encode('echo "abcde;'),
// Press left arrow
27, 91, 68,
27, 91, 68,
27, 91, 68,
27, 91, 68,
...new TextEncoder().encode('___"; '),
10,
]); Outputs: |
It might in RAW mode, but then backspace and other ASCII control characters would get passed – and PHP doesn't seem to handle them correctly:
There may be no easy fix here. I'm now thinking the problem is having node.js between terminal and php – it's probably not forwarding all the required syscalls and signals both ways. Maybe compiling PHP to WASI will solve the problem? |
Actually – since it's fine to only send full lines to PHP, maybe it's okay to wrap that prompt in Node.js readline module: https://gist.github.com/dpyro/d94bb85d284cd91ed156db0404f76e7e |
libedit seems to be calling
This sounds like node.js stdin raw mode after all. The implementation does this:
|
Here's a few more excerpt from libedit's code: libedit_private void
read_prepare(EditLine *el)
{
if (el->el_flags & HANDLE_SIGNALS)
sig_set(el);
if (el->el_flags & NO_TTY)
return;
if ((el->el_flags & (UNBUFFERED|EDIT_DISABLED)) == UNBUFFERED)
tty_rawmode(el);
/* This is relatively cheap, and things go terribly wrong if
we have the wrong size. */
el_resize(el);
re_clear_display(el); /* reset the display stuff */
ch_reset(el);
re_refresh(el); /* print the prompt */
if (el->el_flags & UNBUFFERED)
terminal__flush(el);
} tty_rawmode(EditLine *el)
{
if (el->el_tty.t_mode == ED_IO || el->el_tty.t_mode == QU_IO)
return 0;
if (el->el_flags & EDIT_DISABLED)
return 0;
if (tty_getty(el, &el->el_tty.t_ts) == -1) {
#ifdef DEBUG_TTY
(void) fprintf(el->el_errfile, "%s: tty_getty: %s\n", __func__,
strerror(errno));
#endif /* DEBUG_TTY */
return -1;
}
/*
* We always keep up with the eight bit setting and the speed of the
* tty. But we only believe changes that are made to cooked mode!
*/
el->el_tty.t_eight = tty__geteightbit(&el->el_tty.t_ts);
el->el_tty.t_speed = tty__getspeed(&el->el_tty.t_ts);
if (tty__getspeed(&el->el_tty.t_ex) != el->el_tty.t_speed ||
tty__getspeed(&el->el_tty.t_ed) != el->el_tty.t_speed) {
(void) cfsetispeed(&el->el_tty.t_ex, el->el_tty.t_speed);
(void) cfsetospeed(&el->el_tty.t_ex, el->el_tty.t_speed);
(void) cfsetispeed(&el->el_tty.t_ed, el->el_tty.t_speed);
(void) cfsetospeed(&el->el_tty.t_ed, el->el_tty.t_speed);
}
if (tty__cooked_mode(&el->el_tty.t_ts)) {
int i;
for (i = MD_INP; i <= MD_LIN; i++)
tty_update_flags(el, i);
if (tty__gettabs(&el->el_tty.t_ex) == 0)
el->el_tty.t_tabs = 0;
else
el->el_tty.t_tabs = EL_CAN_TAB ? 1 : 0;
tty__getchar(&el->el_tty.t_ts, el->el_tty.t_c[TS_IO]);
/*
* Check if the user made any changes.
* If he did, then propagate the changes to the
* edit and execute data structures.
*/
for (i = 0; i < C_NCC; i++)
if (el->el_tty.t_c[TS_IO][i] !=
el->el_tty.t_c[EX_IO][i])
break;
if (i != C_NCC) {
/*
* Propagate changes only to the unlibedit_private
* chars that have been modified just now.
*/
for (i = 0; i < C_NCC; i++)
tty_update_char(el, ED_IO, i);
tty_bind_char(el, 0);
tty__setchar(&el->el_tty.t_ed, el->el_tty.t_c[ED_IO]);
for (i = 0; i < C_NCC; i++)
tty_update_char(el, EX_IO, i);
tty__setchar(&el->el_tty.t_ex, el->el_tty.t_c[EX_IO]);
}
}
if (tty_setty(el, TCSADRAIN, &el->el_tty.t_ed) == -1) {
#ifdef DEBUG_TTY
(void) fprintf(el->el_errfile, "%s: tty_setty: %s\n", __func__,
strerror(errno));
#endif /* DEBUG_TTY */
return -1;
}
el->el_tty.t_mode = ED_IO;
return 0;
} It does look like it needs to interface directly with the terminal, or at a very least, have Node.js implementation of these
It doesn't help that it defines POSIX signal handlers that Emscripten does not support at the time:
TL;DR some ground work would be required for full readline/libedit support and wrapping in node.js On the other hand, the only problem with void
rl_callback_read_char(void)
{
int count = 0, done = 0;
const char *buf = el_gets(e, &count);
char *wbuf;
el_set(e, EL_UNBUFFERED, 1);
if (buf == NULL || count-- <= 0)
return;
if (count == 0 && buf[0] == e->el_tty.t_c[TS_IO][C_EOF])
done = 1;
if (buf[count] == '\n' || buf[count] == '\r')
done = 2;
if (done && rl_linefunc != NULL) {
el_set(e, EL_UNBUFFERED, 0);
if (done == 2) {
if ((wbuf = strdup(buf)) != NULL)
wbuf[count] = '\0';
RL_SETSTATE(RL_STATE_DONE);
} else
wbuf = NULL;
(*(void (*)(const char *))rl_linefunc)(wbuf);
}
_rl_update_pos();
} It sets the raw mode and then returns back to the regular mode! PHP.wasm currently doesn't have this kind of control over stdin and so it would need much better terminal support before readline can do its job. |
One more thing, raw mode seems to be just a flag that changes how the input bytes are internally handled: // This sets the raw mode
static tcflag_t
tty_update_flag(EditLine *el, tcflag_t f, int mode, int kind)
{
f &= ~el->el_tty.t_t[mode][kind].t_clrmask;
f |= el->el_tty.t_t[mode][kind].t_setmask;
return f;
} However, there are other functions that call termios directly: /* tty_getty():
* Wrapper for tcgetattr to handle EINTR
*/
static int
tty_getty(EditLine *el, struct termios *t)
{
int rv;
while ((rv = tcgetattr(el->el_infd, t)) == -1 && errno == EINTR)
continue;
return rv;
}
/* tty_setty():
* Wrapper for tcsetattr to handle EINTR
*/
static int
tty_setty(EditLine *el, int action, const struct termios *t)
{
int rv;
while ((rv = tcsetattr(el->el_infd, action, t)) == -1 && errno == EINTR)
continue;
return rv;
} termios.c then deals with ioctl() which is somewhat supported by emscripten, but not to the point where it can actually change parameters of Node.js TTYWrap, however, doesn't support setting these options directly but only through wrappers. It also uses termios and ioctl, but the nuances very likely differ from what Node.js readline.module seems to implement many libedit`s features using mostly JavaScript. It consumes input bytes, parses xterm escape codes, and even does the same raw mode switching as we've seen in libedit code earlier in this thread: Therefore, I can only see three solutions:
|
### Description Adds support for CLI SAPI and networking in node.js: ``` > npm run build > node ./build-cli/php-cli.js -r 'echo "Hello from PHP !";' Hello from PHP ! > node ./build-cli/php-cli.js -r 'echo substr(file_get_contents("https://wordpress.org"), 0, 16);' <!DOCTYPE html> > node ./build-cli/php-cli.js -r 'echo phpversion();' 8.2.0-dev > PHP=5.6 node ./build-cli/php-cli.js -r 'echo phpversion();' 5.6.40 ``` ### Highlights: * Networking is supported (including MySQL and HTTPS) * Switching PHP versions is supported. * [Most WordPress PHPUnit tests pass](#111). The failures are caused by missing extensions and a few misconfigured settings * PHP Interactive mode is supported but [the arrow keys don't work](#118) * `wp-cli` works ### In broad strokes: * CLI SAPI is compiled with libedit (readline replacement) and ncurses. * Network calls are asynchronous. Emscripten's Asyncify enables calling asynchronous code from synchronous code. TCP sockets are shimmed with a WebSocket connection to a built-in proxy server running on localhost. It supports data transfer, arbitrary connection targets, and setting a few TCP socket options. * PHP's OpenSSL uses the same CA certs as Node.js * PHP 5.6 is patched to work with OpenSSL 1.1.0 and many other small patches are introduced. For more details, see [patches overview](#119 (comment)), Dockerfile, and `phpwasm-emscripten-library.js` ### Future work: * PHP Interactive server isn't supported yet. Adding support is a matter of making the incoming connection polling non-blocking using Asyncify. * Use a more recent OpenSSL version * [Better support for CLI interactive mode](#118)
Hey @adamziel, per our conversation, who knows if this could be of use: |
### Description Adds support for CLI SAPI and networking in node.js: ``` > npm run build > node ./build-cli/php-cli.js -r 'echo "Hello from PHP !";' Hello from PHP ! > node ./build-cli/php-cli.js -r 'echo substr(file_get_contents("https://wordpress.org"), 0, 16);' <!DOCTYPE html> > node ./build-cli/php-cli.js -r 'echo phpversion();' 8.2.0-dev > PHP=5.6 node ./build-cli/php-cli.js -r 'echo phpversion();' 5.6.40 ``` ### Highlights: * Networking is supported (including MySQL and HTTPS) * Switching PHP versions is supported. * [Most WordPress PHPUnit tests pass](WordPress/wordpress-playground#111). The failures are caused by missing extensions and a few misconfigured settings * PHP Interactive mode is supported but [the arrow keys don't work](WordPress/wordpress-playground#118) * `wp-cli` works ### In broad strokes: * CLI SAPI is compiled with libedit (readline replacement) and ncurses. * Network calls are asynchronous. Emscripten's Asyncify enables calling asynchronous code from synchronous code. TCP sockets are shimmed with a WebSocket connection to a built-in proxy server running on localhost. It supports data transfer, arbitrary connection targets, and setting a few TCP socket options. * PHP's OpenSSL uses the same CA certs as Node.js * PHP 5.6 is patched to work with OpenSSL 1.1.0 and many other small patches are introduced. For more details, see [patches overview](WordPress/wordpress-playground#119 (comment)), Dockerfile, and `phpwasm-emscripten-library.js` ### Future work: * PHP Interactive server isn't supported yet. Adding support is a matter of making the incoming connection polling non-blocking using Asyncify. * Use a more recent OpenSSL version * [Better support for CLI interactive mode](WordPress/wordpress-playground#118)
What is the problem?
Running PHP CLI in an interactive mode (
-a
) does not support certain keys:ctrl+a
,ctrl+e
are printed as^A
or^E
^[[D
Presumably, it's because PHP.wasm does not have the same kind of access to the input and output streams as a native build does. Notably, PHP.wasm is compiled with
libedit
andncurses
and uses thexterm
TERMINFO database.Investigation
The PHP runtime is connected to host's stdin through Emscripten's NODERAWFS.
At the same time, the wrapping node.js process seems to buffer the stdin input and handle the key inputs on its own. Nothing gets sent to the PHP process until the
enter
key is pressed – I confirmed this by addingconsole.log()
calls to theNODERAWFS.read
function.Once the relevant key codes reach PHP, they are somewhat processed. Here I pressed
123456789←←←←ab<ctrl+a>1_
and12345ab1_
was printed:The arrow keys clearly worked, but the
ab
overwritten67
instead of being inserted before. Also, thectrl+a
key combination did not return to the beginning of the line.Manually sending raw bytes
I thought maybe something's wrong with reading stdin and provided manually sent raw bytes throught stdin:
It did correctly move the caret to the left and the input buffer seemed to contain
echo "a"def;
. Unfortunately, that's not what got rendered in the terminal.stdin.setRawMode(true)
Node.js can be told to stop buffering the input lines and just pass through any bytes it receives as follows:
Weirdly, that stops printing the line buffer to stdout:
I tried manually printing characters to
process.stdout
but didn't get anywhere – PHP CLI does a lot of work there to, e.g., replace the current line buffer with the last history entry when the up arrow is pressed.The text was updated successfully, but these errors were encountered: