Skip to content

Commit e81ae63

Browse files
simply use the built-in php-cli (#1757)
This uses a new exported `do_php_cli()` function in php 8.6 that runs the full (real) CLI from bootup to finish. --------- Signed-off-by: Robert Landers <landers.robert@gmail.com> Signed-off-by: Marc <m@pyc.ac> Co-authored-by: Marc <m@pyc.ac>
1 parent a765b08 commit e81ae63

10 files changed

Lines changed: 261 additions & 197 deletions

File tree

caddy/php-cli.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package caddy
22

33
import (
4-
"errors"
54
"os"
65
"path/filepath"
6+
"strings"
77

88
caddycmd "github.com/caddyserver/caddy/v2/cmd"
99
"github.com/dunglas/frankenphp"
@@ -26,23 +26,16 @@ Executes a PHP script similarly to the CLI SAPI.`,
2626
}
2727

2828
func cmdPHPCLI(fs caddycmd.Flags) (int, error) {
29-
args := os.Args[2:]
30-
if len(args) < 1 {
31-
return 1, errors.New("the path to the PHP script is required")
32-
}
29+
// php's cli sapi expects the 0th arg to be the program itself, only filter out 'php-cli' arg
30+
args := append([]string{os.Args[0]}, os.Args[2:]...)
3331

34-
if frankenphp.EmbeddedAppPath != "" {
35-
if _, err := os.Stat(args[0]); err != nil {
36-
args[0] = filepath.Join(frankenphp.EmbeddedAppPath, args[0])
32+
if frankenphp.EmbeddedAppPath != "" && len(args) > 1 && !strings.HasPrefix(args[1], "-") && strings.HasSuffix(args[1], ".php") {
33+
if _, err := os.Stat(args[1]); err != nil {
34+
args[1] = filepath.Join(frankenphp.EmbeddedAppPath, args[1])
3735
}
3836
}
3937

40-
var status int
41-
if len(args) >= 2 && args[0] == "-r" {
42-
status = frankenphp.ExecutePHPCode(args[1])
43-
} else {
44-
status = frankenphp.ExecuteScriptCLI(args[0], args)
45-
}
38+
status := frankenphp.ExecuteScriptCLI(args[0], args)
4639

4740
os.Exit(status)
4841

cgo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ package frankenphp
66
// #cgo unix LDFLAGS: -lphp -lm -lutil
77
// #cgo linux LDFLAGS: -ldl -lresolv
88
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -liconv -ldl
9-
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256
9+
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256 -DENABLE_INTSAFE_SIGNED_FUNCTIONS
1010
// #cgo windows LDFLAGS: -lpthreadVC3
1111
import "C"

cli.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,3 @@ func ExecuteScriptCLI(script string, args []string) int {
1818

1919
return int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0])), false))
2020
}
21-
22-
func ExecutePHPCode(phpCode string) int {
23-
// Ensure extensions are registered before CLI execution
24-
registerExtensions()
25-
26-
cCode := C.CString(phpCode)
27-
defer C.free(unsafe.Pointer(cCode))
28-
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
29-
}

cli_test.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,34 @@ func TestExecuteCLICode(t *testing.T) {
4646
assert.Equal(t, stdoutStderrStr, `Hello World`)
4747
}
4848

49+
// `-i` (and any other invocation without a script) is only supported since PHP
50+
// 8.6, where the real CLI SAPI is reused. older versions must fail cleanly.
51+
func TestExecuteCLIPHPInfo(t *testing.T) {
52+
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
53+
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
54+
}
55+
56+
cmd := exec.Command("internal/testcli/testcli", "-i")
57+
stdoutStderr, err := cmd.CombinedOutput()
58+
stdoutStderrStr := string(stdoutStderr)
59+
60+
if frankenphp.Version().VersionID < 80600 {
61+
assert.Error(t, err)
62+
63+
var exitError *exec.ExitError
64+
if errors.As(err, &exitError) {
65+
assert.Equal(t, 1, exitError.ExitCode())
66+
}
67+
68+
assert.Contains(t, stdoutStderrStr, "this functionality is not available in frankenphp php-cli")
69+
70+
return
71+
}
72+
73+
assert.NoError(t, err, "output: %s", stdoutStderrStr)
74+
assert.Contains(t, stdoutStderrStr, "PHP Version => "+frankenphp.Version().Version)
75+
}
76+
4977
// Regression test for https://github.com/php/frankenphp/issues/1902. A
5078
// long-running CLI script that installs pcntl_signal handlers must
5179
// receive its own signals reliably
@@ -73,5 +101,5 @@ func ExampleExecuteScriptCLI() {
73101
os.Exit(1)
74102
}
75103

76-
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
104+
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[0], os.Args))
77105
}

emulate_php_cli.c

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#include "emulate_php_cli.h"
2+
#include "frankenphp.h"
3+
#include <SAPI.h>
4+
#include <Zend/zend_alloc.h>
5+
#include <Zend/zend_exceptions.h>
6+
#include <Zend/zend_interfaces.h>
7+
#include <Zend/zend_types.h>
8+
#include <errno.h>
9+
#include <ext/spl/spl_exceptions.h>
10+
#include <ext/standard/head.h>
11+
#include <inttypes.h>
12+
#include <php.h>
13+
#ifdef PHP_WIN32
14+
#include <config.w32.h>
15+
#else
16+
#include <php_config.h>
17+
#endif
18+
#include <php_ini.h>
19+
#include <php_main.h>
20+
#include <php_output.h>
21+
#include <php_variables.h>
22+
#include <php_version.h>
23+
#include <pthread.h>
24+
#include <sapi/embed/php_embed.h>
25+
#include <signal.h>
26+
#include <stdint.h>
27+
#include <stdio.h>
28+
#include <stdlib.h>
29+
#include <string.h>
30+
#ifndef ZEND_WIN32
31+
#include <unistd.h>
32+
#endif
33+
#if defined(__linux__)
34+
#include <sys/prctl.h>
35+
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
36+
#include <pthread_np.h>
37+
#endif
38+
39+
cli_exec_args_t *cli_args;
40+
41+
static void register_server_variable_filtered(const char *key, char **val,
42+
size_t *val_len,
43+
zval *track_vars_array) {
44+
if (sapi_module.input_filter(PARSE_SERVER, key, val, *val_len, val_len)) {
45+
php_register_variable_safe(key, *val, *val_len, track_vars_array);
46+
}
47+
}
48+
49+
/*
50+
* CLI code is adapted from
51+
* https://github.com/php/php-src/blob/master/sapi/cli/php_cli.c Copyright (c)
52+
* The PHP Group Licensed under The PHP License Original uthors: Edin Kadribasic
53+
* <edink@php.net>, Marcus Boerger <helly@php.net> and Johannes Schlueter
54+
* <johannes@php.net> Parts based on CGI SAPI Module by Rasmus Lerdorf, Stig
55+
* Bakken and Zeev Suraski
56+
*/
57+
static void cli_register_file_handles(bool no_close) /* {{{ */
58+
{
59+
php_stream *s_in, *s_out, *s_err;
60+
php_stream_context *sc_in = NULL, *sc_out = NULL, *sc_err = NULL;
61+
zend_constant ic, oc, ec;
62+
63+
s_in = php_stream_open_wrapper_ex("php://stdin", "rb", 0, NULL, sc_in);
64+
s_out = php_stream_open_wrapper_ex("php://stdout", "wb", 0, NULL, sc_out);
65+
s_err = php_stream_open_wrapper_ex("php://stderr", "wb", 0, NULL, sc_err);
66+
67+
if (s_in == NULL || s_out == NULL || s_err == NULL) {
68+
if (s_in)
69+
php_stream_close(s_in);
70+
if (s_out)
71+
php_stream_close(s_out);
72+
if (s_err)
73+
php_stream_close(s_err);
74+
return;
75+
}
76+
77+
if (no_close) {
78+
s_in->flags |= PHP_STREAM_FLAG_NO_CLOSE;
79+
s_out->flags |= PHP_STREAM_FLAG_NO_CLOSE;
80+
s_err->flags |= PHP_STREAM_FLAG_NO_CLOSE;
81+
}
82+
83+
/*s_in_process = s_in;*/
84+
85+
php_stream_to_zval(s_in, &ic.value);
86+
php_stream_to_zval(s_out, &oc.value);
87+
php_stream_to_zval(s_err, &ec.value);
88+
89+
ZEND_CONSTANT_SET_FLAGS(&ic, CONST_CS, 0);
90+
ic.name = zend_string_init_interned("STDIN", sizeof("STDIN") - 1, 0);
91+
zend_register_constant(&ic);
92+
93+
ZEND_CONSTANT_SET_FLAGS(&oc, CONST_CS, 0);
94+
oc.name = zend_string_init_interned("STDOUT", sizeof("STDOUT") - 1, 0);
95+
zend_register_constant(&oc);
96+
97+
ZEND_CONSTANT_SET_FLAGS(&ec, CONST_CS, 0);
98+
ec.name = zend_string_init_interned("STDERR", sizeof("STDERR") - 1, 0);
99+
zend_register_constant(&ec);
100+
}
101+
/* }}} */
102+
103+
static void sapi_cli_register_variables(zval *track_vars_array) /* {{{ */
104+
{
105+
size_t len = strlen(cli_args->script);
106+
char *docroot = "";
107+
108+
/*
109+
* In CGI mode, we consider the environment to be a part of the server
110+
* variables
111+
*/
112+
php_import_environment_variables(track_vars_array);
113+
114+
/* Build the special-case PHP_SELF variable for the CLI version */
115+
register_server_variable_filtered("PHP_SELF", &cli_args->script, &len,
116+
track_vars_array);
117+
register_server_variable_filtered("SCRIPT_NAME", &cli_args->script, &len,
118+
track_vars_array);
119+
120+
/* filenames are empty for stdin */
121+
register_server_variable_filtered("SCRIPT_FILENAME", &cli_args->script, &len,
122+
track_vars_array);
123+
register_server_variable_filtered("PATH_TRANSLATED", &cli_args->script, &len,
124+
track_vars_array);
125+
126+
/* just make it available */
127+
len = 0U;
128+
register_server_variable_filtered("DOCUMENT_ROOT", &docroot, &len,
129+
track_vars_array);
130+
}
131+
/* }}} */
132+
133+
void *emulate_script_cli(void *arg) {
134+
void *exit_status;
135+
cli_exec_args_t *args = arg;
136+
cli_args = args;
137+
138+
/* Parse argv to detect -r (eval mode) and find the script path */
139+
bool eval = false;
140+
char *script = NULL;
141+
for (int i = 1; i < args->argc; i++) {
142+
if (strcmp(args->argv[i], "-r") == 0 && i + 1 < args->argc) {
143+
eval = true;
144+
script = args->argv[i + 1];
145+
break;
146+
} else if (args->argv[i][0] != '-') {
147+
script = args->argv[i];
148+
break;
149+
}
150+
}
151+
152+
if (script == NULL) {
153+
fprintf(stderr, "this functionality is not available in frankenphp php-cli "
154+
"with a php version lower than 8.6\n");
155+
return (void *)(intptr_t)1;
156+
}
157+
158+
/* Update cli_args->script so sapi_cli_register_variables uses the right path
159+
*/
160+
cli_args->script = script;
161+
162+
/*
163+
* The SAPI name "cli" is hardcoded into too many programs... let's usurp it.
164+
*/
165+
php_embed_module.name = "cli";
166+
php_embed_module.pretty_name = "PHP CLI embedded in FrankenPHP";
167+
php_embed_module.register_server_variables = sapi_cli_register_variables;
168+
169+
php_embed_init(cli_args->argc, cli_args->argv);
170+
171+
cli_register_file_handles(false);
172+
zend_first_try {
173+
if (eval) {
174+
/* evaluate script as literal PHP code (php-cli -r "...") */
175+
zend_eval_string_ex(script, NULL, "Command line code", 1);
176+
} else {
177+
zend_file_handle file_handle;
178+
zend_stream_init_filename(&file_handle, script);
179+
180+
CG(skip_shebang) = 1;
181+
php_execute_script(&file_handle);
182+
}
183+
}
184+
zend_end_try();
185+
186+
exit_status = (void *)(intptr_t)EG(exit_status);
187+
188+
php_embed_shutdown();
189+
190+
return exit_status;
191+
}

emulate_php_cli.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#ifndef _EMULATE_PHP_CLI_H
2+
#define _EMULATE_PHP_CLI_H
3+
4+
#include <stdbool.h>
5+
6+
typedef struct {
7+
char *script;
8+
int argc;
9+
char **argv;
10+
bool eval;
11+
} cli_exec_args_t;
12+
extern cli_exec_args_t *cli_args;
13+
void *emulate_script_cli(void *arg);
14+
15+
#endif

0 commit comments

Comments
 (0)