Problem description
I would like to open a Linux pseudoterminal with PHP but it seems there is no simple way of doing that. I have experimented with different solutions but none seem to be good enough.
The goal of the PTY is to emulate a terminal with the capability of flawlessly interacting with programs such as zsh and sudo. Other programming languages including Python and C have functions or libraries for that. Python has the PTY library which can simply do pty.spawn("/bin/zsh"), and C has the openpty() function.
My ideal end goal is to have a PHP function that allows me to read and write from/into the PTY terminal and does not require installing external libraries. (A lot of shared hosting providers do not allow that.)
What I have tried so far
Using proc_open()
My initial idea was just to use the proc_open() PHP function to create a Bash terminal with stdin, stdout and stderr pipes (Based on Example #1 in the PHP documentation) This, however, did soon prove to be problematic because it is actually not a real PTY. Running stty -a errored with stty: stdin isn't a terminal. Here are the instructions for reproducing this.
- Run this with
php pty_test.php - Read the output of the shell with
cat /tmp/stdout. - Input commands with
> /tmp/stdin.
Here is the PHP code which I used for this:
<?php
/* pty_test.php */
ini_set('display_errors', 1);
ini_set('display_startup_ūūerrors', 1);
error_reporting(E_ALL);
define("STD_IN", 0);
define("STD_OUT", 1);
define("STD_ERR", 2);
set_time_limit(0);
umask(0);
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = "/bin/sh -i ";
$stdin_fifo = "/tmp/stdin";
$stdout_fifo = "/tmp/stdout";
posix_mkfifo($stdin_fifo, 0644);
posix_mkfifo($stdout_fifo, 0644);
$resource_stdin = fopen($stdin_fifo, "rb+");
$resource_stdout = fopen($stdout_fifo, "wb+");
$descriptorspec = array(
STD_IN => array("pipe", "rb"),
STD_OUT => array("pipe", "wb"),
STD_ERR => array("pipe", "wb")
);
$process = proc_open($shell, $descriptorspec, $pipes, null, $env = null);
stream_set_blocking($pipes[STD_IN], 0);
stream_set_blocking($pipes[STD_OUT], 0);
stream_set_blocking($pipes[STD_ERR], 0);
stream_set_blocking($resource_stdin, 0);
stream_set_blocking($resource_stdout, 0);
while (1) {
$read_a = array($resource_stdin, $pipes[STD_OUT], $pipes[STD_ERR]);
$num_changed_streams = stream_select($read_a, $write_a, $error_a, null);
if (in_array($resource_stdin, $read_a)) {
$input = fread($resource_stdin, $chunk_size);
fwrite($pipes[STD_IN], $input);
}
if (in_array($pipes[STD_OUT], $read_a)) {
$input = fread($pipes[STD_OUT], $chunk_size);
fwrite($resource_stdout, $input);
}
if (in_array($pipes[STD_ERR], $read_a)) {
$input = fread($pipes[STD_ERR], $chunk_size);
fwrite($resource_stdout, $input);
}
}
fclose($resource_stdin);
fclose($resource_stdout);
fclose($pipes[STD_IN]);
fclose($pipes[STD_OUT]);
fclose($pipes[STD_ERR]);
proc_close($process);
unlink($stdin_fifo);
unlink($stdout_fifo);
?>
Python PTY
I noticed that running python3 -c "import pty;pty.spawn('/bin/bash');" in the non-pty shell (which I described above) will result in a fully interactive PTY shell as I desired. This resulted in a half-good solution: setting the $shell variable to be python3 -c "import pty;pty.spawn('/bin/bash')" will spawn the interactive shell using Python3. But relying on external software is not ideal since having Python3 is not always guaranteed. (And this solution also feels way too hacky...)
/dev/ptmx
I was reading the source code of the proc_open() function also found the source for openpty(). Unfortunately, PHP can't directly call this function but perhaps it is possible to replicate the behavior of it.
I could fopen("/dev/ptmx","r+") to create a new slave but openpty() also uses grantpt() and unlockpt(), which are not available in PHP.
Foreign Function Interface
FFI allows access to external libraries. Maybe it would be possible to import pty.h and to run openpty(). Unfortunately, FFI is very experimental and may not always be available.
TL;DR
What is the safest and most reliable way to spawn a PTY using PHP?