diff --git a/README.md b/README.md index 8ba8a098..ae7773e7 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ console.log(response.body.toString()) ### `new Php(config)` * `config` {Object} Configuration object + * `argv` {String[]} Process arguments. **Default:** [] * `docroot` {String} Document root for PHP. **Default:** process.cwd() * Returns: {Php} @@ -67,6 +68,7 @@ Construct a new PHP instance to which to dispatch requests. import { Php } from '@platformatic/php-node' const php = new Php({ + argv: process.argv, docroot: process.cwd() }) ```` diff --git a/__test__/handler.spec.mjs b/__test__/handler.spec.mjs index 60af4679..b5775115 100644 --- a/__test__/handler.spec.mjs +++ b/__test__/handler.spec.mjs @@ -15,12 +15,11 @@ test('Support input/output streams', async (t) => { t.teardown(() => mockroot.clean()) const php = new Php({ - argv: process.argv, docroot: mockroot.path }) const req = new Request({ - method: 'GET', + method: 'POST', url: 'http://example.com/index.php', body: Buffer.from('Hello, from Node.js!') }) @@ -39,12 +38,10 @@ test('Capture logs', async (t) => { t.teardown(() => mockroot.clean()) const php = new Php({ - argv: process.argv, docroot: mockroot.path }) const req = new Request({ - method: 'GET', url: 'http://example.com/index.php' }) @@ -62,12 +59,10 @@ test('Capture exceptions', async (t) => { t.teardown(() => mockroot.clean()) const php = new Php({ - argv: process.argv, docroot: mockroot.path }) const req = new Request({ - method: 'GET', url: 'http://example.com/index.php' }) @@ -89,12 +84,10 @@ test('Support request and response headers', async (t) => { t.teardown(() => mockroot.clean()) const php = new Php({ - argv: process.argv, docroot: mockroot.path }) const req = new Request({ - method: 'GET', url: 'http://example.com/index.php', headers: { 'X-Test': ['Hello, from Node.js!'] @@ -106,3 +99,33 @@ test('Support request and response headers', async (t) => { t.is(res.body.toString(), 'Hello, from Node.js!') t.is(res.headers.get('X-Test'), 'Hello, from PHP!') }) + +test('Has expected args', async (t) => { + const mockroot = await MockRoot.from({ + 'index.php': `` + }) + t.teardown(() => mockroot.clean()) + + const php = new Php({ + argv: process.argv, + docroot: mockroot.path + }) + + const req = new Request({ + url: 'http://example.com/index.php' + }) + + const res = await php.handleRequest(req) + t.is(res.status, 200) + + t.is(res.body.toString('utf8'), JSON.stringify(process.argv)) +}) diff --git a/crates/php/src/embed.rs b/crates/php/src/embed.rs index 7f5d1367..b63a7f43 100644 --- a/crates/php/src/embed.rs +++ b/crates/php/src/embed.rs @@ -1,7 +1,8 @@ use std::{ env::Args, - ffi::c_char, + ffi::{c_char, CString}, path::{Path, PathBuf}, + sync::Arc, }; use ext_php_rs::{ @@ -25,6 +26,14 @@ use crate::{ #[derive(Debug)] pub struct Embed { docroot: PathBuf, + + // TODO: Do something with this... + #[allow(dead_code)] + args: Vec, + + // NOTE: This needs to hold the SAPI to keep it alive + #[allow(dead_code)] + sapi: Arc, } // An embed instance may be constructed on the main thread and then shared @@ -90,14 +99,16 @@ impl Embed { C: AsRef, S: AsRef + std::fmt::Debug, { - ensure_sapi(argv)?; - let docroot = docroot .as_ref() .canonicalize() .map_err(|_| EmbedException::DocRootNotFound(docroot.as_ref().display().to_string()))?; - Ok(Embed { docroot }) + Ok(Embed { + docroot, + args: argv.iter().map(|v| v.as_ref().to_string()).collect(), + sapi: ensure_sapi()?, + }) } /// Get the docroot used for this Embed instance @@ -152,12 +163,8 @@ impl Handler for Embed { /// //assert_eq!(response.body(), "Hello, world!"); /// ``` fn handle(&self, request: Request) -> Result { - unsafe { - ext_php_rs::embed::ext_php_rs_sapi_per_thread_init(); - } - // Initialize the SAPI module - Sapi::startup()?; + self.sapi.startup()?; let url = request.url(); @@ -182,6 +189,14 @@ impl Handler for Embed { .unwrap_or(0); let cookie_data = nullable_cstr(headers.get("Cookie"))?; + let argc = self.args.len() as i32; + let mut argv_ptrs = vec![]; + for arg in self.args.iter() { + let string = CString::new(arg.as_bytes()) + .map_err(|_| EmbedException::CStringEncodeFailed(arg.to_owned()))?; + argv_ptrs.push(string.into_raw()); + } + // Prepare memory stream of the code let mut file_handle = unsafe { let mut file_handle = zend_file_handle { @@ -214,8 +229,8 @@ impl Handler for Embed { // Reset state globals.request_info.proto_num = 110; - globals.request_info.argc = 0; - globals.request_info.argv = std::ptr::null_mut(); + globals.request_info.argc = argc; + globals.request_info.argv = argv_ptrs.as_mut_ptr(); globals.request_info.headers_read = false; globals.sapi_headers.http_response_code = 200; diff --git a/crates/php/src/sapi.rs b/crates/php/src/sapi.rs index e64f9728..764cdf0f 100644 --- a/crates/php/src/sapi.rs +++ b/crates/php/src/sapi.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, env::current_exe, ffi::{c_char, c_int, c_void, CStr}, - sync::RwLock, + sync::{Arc, RwLock, Weak}, }; use bytes::Buf; @@ -12,9 +12,9 @@ use ext_php_rs::{ embed::SapiModule, exception::register_error_observer, ffi::{ - ext_php_rs_sapi_shutdown, ext_php_rs_sapi_startup, php_module_shutdown, php_module_startup, - php_register_variable, sapi_send_headers, sapi_shutdown, sapi_startup, - ZEND_RESULT_CODE_SUCCESS, + ext_php_rs_sapi_per_thread_init, ext_php_rs_sapi_shutdown, ext_php_rs_sapi_startup, + php_module_shutdown, php_module_startup, php_register_variable, sapi_send_headers, + sapi_shutdown, sapi_startup, ZEND_RESULT_CODE_SUCCESS, }, prelude::*, zend::{SapiGlobals, SapiHeader}, @@ -23,30 +23,21 @@ use ext_php_rs::{ use once_cell::sync::OnceCell; use crate::{ - strings::{cstr, maybe_current_dir}, + strings::{cstr, drop_str, maybe_current_dir}, EmbedException, RequestContext, }; use lang_handler::Header; // This is a helper to ensure that PHP is initialized and deinitialized at the // appropriate times. -pub(crate) struct Sapi(Box); +#[derive(Debug)] +pub(crate) struct Sapi(RwLock>); impl Sapi { - pub fn new(argv: Vec) -> Result - where - S: AsRef, - { - let argv: Vec<&str> = argv.iter().map(|s| s.as_ref()).collect(); - // let argc = argv.len() as i32; - // let mut argv_ptrs = argv - // .iter() - // .map(|v| v.as_ptr() as *mut c_char) - // .collect::>(); - + pub fn new() -> Result { let mut sapi = SapiBuilder::new("php_lang_handler", "PHP Lang Handler") .startup_function(sapi_module_startup) - // .shutdown_function(sapi_module_shutdown) + .shutdown_function(sapi_module_shutdown) // .activate_function(sapi_module_activate) .deactivate_function(sapi_module_deactivate) .ub_write_function(sapi_module_ub_write) @@ -66,20 +57,20 @@ impl Sapi { sapi.additional_functions = std::ptr::null(); // sapi.phpinfo_as_text = 1; - let exe_loc = argv - .first() - .map(|s| s.to_string()) - .or_else(|| current_exe().ok().map(|p| p.display().to_string())) - .ok_or(EmbedException::FailedToFindExeLocation)?; + let exe_loc = current_exe() + .map(|p| p.display().to_string()) + .map_err(|_| EmbedException::FailedToFindExeLocation)?; sapi.executable_location = cstr(exe_loc)?; let mut boxed = Box::new(sapi); unsafe { ext_php_rs_sapi_startup(); - sapi_startup(boxed.as_mut()); - php_module_startup(boxed.as_mut(), get_module()); + + if let Some(startup) = boxed.startup { + startup(boxed.as_mut()); + } } // TODO: Should maybe capture this to store in EmbedException rather than @@ -94,54 +85,61 @@ impl Sapi { ctx.response_builder().exception(*msg); } }) + // TODO: Report this error somehow? .ok(); }); - Ok(Sapi(boxed)) + Ok(Sapi(RwLock::new(boxed))) } - fn do_startup(&mut self) -> Result<(), EmbedException> { - let sapi = self.0.as_mut(); - let startup = sapi - .startup - .ok_or(EmbedException::SapiMissingStartupFunction)?; - if unsafe { startup(sapi) } != ZEND_RESULT_CODE_SUCCESS { - return Err(EmbedException::SapiNotStarted); + pub fn startup<'a>(&'a self) -> Result<(), EmbedException> { + unsafe { + ext_php_rs_sapi_per_thread_init(); } - Ok(()) - } - pub fn startup() -> Result<(), EmbedException> { - SAPI_INIT - .get() - .ok_or(EmbedException::SapiNotInitialized) - .and_then(|rwlock| { - let mut sapi = rwlock.write().map_err(|_| EmbedException::SapiLockFailed)?; + let rwlock = &self.0; + let sapi = rwlock.read().map_err(|_| EmbedException::SapiLockFailed)?; - sapi.do_startup()?; - Ok(()) - }) + if let Some(startup) = sapi.startup { + if unsafe { startup(sapi.into_raw()) } != ZEND_RESULT_CODE_SUCCESS { + return Err(EmbedException::SapiNotStarted); + } + } + + Ok(()) } } impl Drop for Sapi { fn drop(&mut self) { unsafe { - php_module_shutdown(); sapi_shutdown(); - ext_php_rs_sapi_shutdown(); } } } -pub(crate) static SAPI_INIT: OnceCell> = OnceCell::new(); +pub(crate) static SAPI_INIT: OnceCell>> = OnceCell::new(); + +pub fn ensure_sapi() -> Result, EmbedException> { + let weak_sapi = SAPI_INIT.get_or_try_init(|| Ok(RwLock::new(Weak::new())))?; -pub fn ensure_sapi(argv: Vec) -> Result<&'static RwLock, EmbedException> -where - S: AsRef + std::fmt::Debug, -{ - SAPI_INIT.get_or_try_init(|| Sapi::new(argv).map(RwLock::new)) + if let Some(sapi) = weak_sapi + .read() + .map_err(|_| EmbedException::SapiLockFailed)? + .upgrade() + { + return Ok(sapi); + } + + let mut rwlock = weak_sapi + .write() + .map_err(|_| EmbedException::SapiLockFailed)?; + + let sapi = Sapi::new().map(Arc::new)?; + *rwlock = Arc::downgrade(&sapi); + + Ok(sapi) } // @@ -166,15 +164,17 @@ static HARDCODED_INI: &str = " "; #[no_mangle] -pub extern "C" fn sapi_cli_ini_defaults(configuration_hash: *mut ext_php_rs::types::ZendHashTable) { - let hash = unsafe { &mut *configuration_hash }; +pub extern "C" fn sapi_cli_ini_defaults(ht: *mut ext_php_rs::types::ZendHashTable) { + let config = unsafe { &mut *ht }; - let config = str::trim(HARDCODED_INI).lines().map(str::trim); + let ini_lines = str::trim(HARDCODED_INI).lines().map(str::trim); - for line in config { + for line in ini_lines { if let Some((key, value)) = line.split_once('=') { + use ext_php_rs::convert::IntoZval; + let value = value.into_zval(true).unwrap(); // TODO: Capture error somehow? - hash.insert(key, value).ok(); + config.insert(key, value).ok(); } } } @@ -186,31 +186,45 @@ pub extern "C" fn sapi_module_startup( unsafe { php_module_startup(sapi_module, get_module()) } } +#[no_mangle] +pub extern "C" fn sapi_module_shutdown( + _sapi_module: *mut SapiModule, +) -> ext_php_rs::ffi::zend_result { + unsafe { + php_module_shutdown(); + } + ZEND_RESULT_CODE_SUCCESS +} + #[no_mangle] pub extern "C" fn sapi_module_deactivate() -> c_int { { let mut globals = SapiGlobals::get_mut(); + for i in 0..globals.request_info.argc { + drop_str(unsafe { *globals.request_info.argv.offset(i as isize) }); + } + globals.server_context = std::ptr::null_mut(); globals.request_info.argc = 0; globals.request_info.argv = std::ptr::null_mut(); - // drop_str(globals.request_info.request_method); - // drop_str(globals.request_info.query_string); - // drop_str(globals.request_info.request_uri); - // drop_str(globals.request_info.path_translated); - // drop_str(globals.request_info.content_type); - // drop_str(globals.request_info.cookie_data); + drop_str(globals.request_info.request_method); + drop_str(globals.request_info.query_string); + drop_str(globals.request_info.request_uri); + drop_str(globals.request_info.path_translated); + drop_str(globals.request_info.content_type); + drop_str(globals.request_info.cookie_data); // drop_str(globals.request_info.php_self); - // drop_str(globals.request_info.auth_user); - // drop_str(globals.request_info.auth_password); - // drop_str(globals.request_info.auth_digest); + drop_str(globals.request_info.auth_user); + drop_str(globals.request_info.auth_password); + drop_str(globals.request_info.auth_digest); } // TODO: When _is_ it safe to reclaim the request context? - // RequestContext::reclaim(); + RequestContext::reclaim(); - 0 + ZEND_RESULT_CODE_SUCCESS } #[no_mangle] @@ -356,7 +370,11 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t sapi .read() .map_err(|_| EmbedException::SapiLockFailed)? + .upgrade() + .ok_or(EmbedException::SapiLockFailed)? .0 + .read() + .map_err(|_| EmbedException::SapiLockFailed)? .name, vars, );