diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 4987a76..ead942c 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -1,5 +1,6 @@ use std::env; use std::fs::read_to_string; +use std::fs::write; use std::fs::File; use std::io::Write; use std::path::PathBuf; @@ -13,16 +14,18 @@ use log::info; use sysinfo::get_current_pid; use sysinfo::ProcessExt; use sysinfo::SystemExt; +use crate::config::config::php_server_pid; +use crate::config::paths::rymfony_server_info_file; +use crate::config::paths::rymfony_pid_file; use crate::http::proxy_server; use crate::php::php_server; -use crate::php::php_server::PhpServer; use crate::php::structs::PhpServerSapi; use crate::php::structs::ServerInfo; use crate::utils::current_process_name; use crate::utils::network::find_available_port; use crate::utils::network::parse_default_port; -use crate::utils::project_directory::get_rymfony_project_directory; +use crate::utils::project_directory::{clean_rymfony_runtime_files, get_rymfony_project_directory}; const DEFAULT_PORT: &str = "8000"; @@ -92,45 +95,52 @@ pub(crate) fn serve(args: &ArgMatches) { } fn serve_foreground(args: &ArgMatches) { - let path = get_rymfony_project_directory().unwrap(); - let rymfony_pid_file = path.join("rymfony.pid"); - debug!( - "Looking for PID file in \"{}\".", - rymfony_pid_file.to_str().unwrap() - ); + let rymfony_pid_file = rymfony_pid_file(); + debug!("Looking for Rymfony PID file in \"{}\".",rymfony_pid_file.to_str().unwrap()); + if rymfony_pid_file.exists() { // Check if process is rymfony and exit if true. - let infos: ServerInfo = - serde_json::from_str(read_to_string(&rymfony_pid_file).unwrap().as_str()) - .expect("Unable to unserialize data from PID file."); + let server_info_file = rymfony_server_info_file(); + + if !server_info_file.exists() { + warn!("Rymfony's PID file exists, but no server info was found."); + warn!("Cleaning Rymfony's project directory."); + clean_rymfony_runtime_files(); + } else { + let infos: ServerInfo = + serde_json::from_str(read_to_string(server_info_file).unwrap().as_str()) + .expect("Unable to unserialize data from PID file."); - let mut system = sysinfo::System::new_all(); - system.refresh_all(); - for (pid, proc_) in system.get_processes() { - #[cfg(not(target_family = "windows"))] - let process_pid = *pid; + let php_server_pid = php_server_pid(); - #[cfg(target_family = "windows")] - let process_pid = *pid as i32; + let mut system = sysinfo::System::new_all(); + system.refresh_all(); + for (pid, proc_) in system.get_processes() { + #[cfg(not(target_family = "windows"))] + let process_pid = *pid; - let mut pname = proc_.exe().to_str().unwrap(); - let pname_lower = pname.to_lowercase(); - pname = pname_lower.as_str(); + #[cfg(target_family = "windows")] + let process_pid = *pid as i32; - let exe_rymfony_name = if cfg!(not(target_family = "windows")) { - "rymfony" - } else { - "rymfony.exe" - }; + let mut pname = proc_.exe().to_str().unwrap(); + let pname_lower = pname.to_lowercase(); + pname = pname_lower.as_str(); - if &process_pid == &infos.pid() && pname.ends_with(exe_rymfony_name) { - info!( + let exe_rymfony_name = if cfg!(not(target_family = "windows")) { + "rymfony" + } else { + "rymfony.exe" + }; + + if process_pid.to_string() == php_server_pid && pname.ends_with(exe_rymfony_name) { + info!( "The server is already running and listening to {}://127.0.0.1:{}", infos.scheme(), infos.port() ); - return; + return; + } } } } @@ -169,14 +179,14 @@ fn serve_foreground(args: &ArgMatches) { }; let php_entrypoint_path = doc_root_path.join(script_filename.as_str()); - let php_server = if !php_entrypoint_path.is_file() { + let (php_sapi, php_server_port) = if !php_entrypoint_path.is_file() { warn!("No PHP entrypoint file"); - PhpServer::new(0, PhpServerSapi::Unknown) + (PhpServerSapi::Unknown, 0) } else { php_server::start() }; - let sapi = match php_server.sapi() { + let sapi = match php_sapi { PhpServerSapi::FPM => "FPM", PhpServerSapi::CLI => "CLI", PhpServerSapi::CGI => "CGI", @@ -204,6 +214,8 @@ fn serve_foreground(args: &ArgMatches) { #[cfg(target_family = "windows")] let pid = get_current_pid().unwrap() as i32; + write(&rymfony_pid_file, pid.to_string()).expect("Could not write Rymfony PID to file."); + let args_str: Vec = Vec::new(); let scheme = if args.is_present("no-tls") { "http".to_string() @@ -211,7 +223,6 @@ fn serve_foreground(args: &ArgMatches) { "https".to_string() }; let pid_info = ServerInfo::new( - pid, port, scheme, "Web Server".to_string(), @@ -230,7 +241,7 @@ fn serve_foreground(args: &ArgMatches) { proxy_server::start( !args.is_present("no-tls"), port, - php_server.port(), + php_server_port, document_root, script_filename, args.is_present("expose-server-header"), diff --git a/src/config/config.rs b/src/config/config.rs index 9fa6c30..86d3f09 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -8,6 +8,7 @@ use std::fs::read_to_string; use std::fs::remove_file; use std::fs::File; use std::io::Write; +use crate::config::paths::php_server_pid_file; #[derive(Debug)] struct ConfigError(String); @@ -71,3 +72,7 @@ pub(crate) fn clear_binaries_list() -> std::result::Result<(), Box String { + String::from(read_to_string(php_server_pid_file()).expect("Could not read PHP server's PID file.")) +} diff --git a/src/config/paths.rs b/src/config/paths.rs new file mode 100644 index 0000000..fa39806 --- /dev/null +++ b/src/config/paths.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; +use crate::utils::project_directory::get_rymfony_project_directory; + +pub(crate) fn rymfony_pid_file() -> PathBuf { + PathBuf::from(format!("{}/rymfony.pid", get_rymfony_project_directory().unwrap().to_str().unwrap())) +} + +pub(crate) fn rymfony_server_info_file() -> PathBuf { + PathBuf::from(format!("{}/rymfony_server_info", get_rymfony_project_directory().unwrap().to_str().unwrap())) +} + +pub(crate) fn php_server_pid_file() -> PathBuf { + PathBuf::from(format!("{}/php_server.pid", get_rymfony_project_directory().unwrap().to_str().unwrap())) +} + +#[cfg(not(target_os = "windows"))] +pub(crate) fn php_fpm_conf_ini_file() -> PathBuf { + PathBuf::from(format!("{}/fpm-conf.ini", get_rymfony_project_directory().unwrap().to_str().unwrap())) +} diff --git a/src/http/proxy_server.rs b/src/http/proxy_server.rs index af4fd94..f6e3636 100644 --- a/src/http/proxy_server.rs +++ b/src/http/proxy_server.rs @@ -86,17 +86,19 @@ pub(crate) fn start( if output.status.code().unwrap() != 0 { let stderr = String::from_utf8(output.stderr).unwrap(); + if stderr.contains("listen tcp :80: bind: permission denied") { error!("Caddy is unable to listen to port 80, which is used for HTTP to HTTPS redirection."); error!("This can happen when you run Caddy (and therefore Rymfony) as non-root user."); error!("To make it work, you need to give Caddy the necessary network capabilities."); #[cfg(target_os = "linux")] { - error!("On most linux distribuions, you can do it by running this command (possibly with \"sudo\"):"); + error!("On most linux distributions, you can do it by running this command (possibly with \"sudo\"):"); error!(" setcap cap_net_bind_service=+ep {}", caddy_path.to_str().unwrap()); } } - panic!("Caddy failed to start."); + + panic!("Caddy failed to start with error:\n{}", stderr); } } diff --git a/src/main.rs b/src/main.rs index 6d05ec1..6f0633b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ extern crate regex; mod config { pub(crate) mod config; + pub(crate) mod paths; } mod commands { @@ -30,7 +31,6 @@ mod php { pub(crate) mod php_server; pub(crate) mod server_cgi; pub(crate) mod server_fpm; - pub(crate) mod server_native; pub(crate) mod structs; } diff --git a/src/php/binaries.rs b/src/php/binaries.rs index 9eac93b..afc97ee 100644 --- a/src/php/binaries.rs +++ b/src/php/binaries.rs @@ -241,7 +241,7 @@ fn binaries_from_dir(path: PathBuf) -> HashMap { } else { let mut bin = PhpBinary::from_version(version.clone()); bin.add_sapi(&sapi, &path); - &binaries.insert(version.clone(), bin); + let _ = &binaries.insert(version.clone(), bin); } } @@ -293,7 +293,7 @@ fn merge_binaries(into: &mut HashMap, from: HashMap PhpServer { - PhpServer { port, sapi } - } - - pub fn port(&self) -> u16 { - self.port - } - - pub fn sapi(&self) -> &PhpServerSapi { - &self.sapi - } -} +const PHP_DEFAULT_PORT: u16 = 60000; -pub(crate) fn start() -> PhpServer { +pub(crate) fn start() -> (PhpServerSapi, u16) { let php_bin = binaries::get_project_version(); let phpbin_path = PathBuf::from(php_bin.as_str()); @@ -51,78 +34,95 @@ pub(crate) fn start() -> PhpServer { panic!("Unable to start the required PHP binary"); } - let (php_server, mut process) = - if php_bin.contains("-fpm") && cfg!(not(target_family = "windows")) { - start_fpm(php_bin.clone()) - } else if php_bin.contains("-cgi") { - start_cgi(php_bin.clone()) - } else { - start_native(php_bin.clone()) - }; + let php_server_port = find_available_port(PHP_DEFAULT_PORT); + + let (php_server_sapi, mut command) = start_php_server(&php_bin, &php_server_port); let sleep_time = time::Duration::from_millis(1000); thread::sleep(sleep_time); + let mut process = command.spawn().expect("Could not start PHP server."); let process_status = process.try_wait(); match process_status { Ok(Some(status)) => panic!("PHP server exited with {}", status), Ok(None) => { - info!("PHP server is ready"); + info!("PHP server is ready and listening to port {}", &php_server_port); } Err(e) => panic!("An error occured when checking PHP server health: {:?}", e), } let process_pid = process.id(); + let args_str: Vec = Vec::new(); + let process_pid_string = if process_pid > 0 { + process_pid.to_string() + } else { + panic!("Could not retrieve PHP server's PID. Maybe the server has failed to start, or stopped right after starting."); + }; + + write(php_server_pid_file(), &process_pid_string).expect("Could not write PHP server PID to file."); + ctrlc::set_handler(move || { info!("Stopping PHP process..."); - #[cfg(not(target_os = "windows"))] - { - let pid = process.id(); - stop_process::stop(pid.to_string().as_ref()); // Stop fpm children - } + let pid = php_server_pid(); + stop_process::stop(&pid); // Stop fpm children + info!("PHP process stopped."); - match process.kill() { - Ok(_) => info!("PHP process stopped."), - Err(e) => error!("An error occured when trying to stop PHP: {:?}", e), - } - process::exit(0); + clean_rymfony_runtime_files(); + info!("Cleaned Rymfony runtime files."); }) .expect("Error setting Ctrl-C handler"); - let args_str: Vec = Vec::new(); - let pidstr = if process_pid > 0 { - process_pid.to_string() - } else { - "0".to_string() - }; - let pid_info = ServerInfo::new( - i32::from_str(pidstr.as_str()).unwrap(), - php_server.port, + php_server_port, "".to_string(), - format!("{}", php_server.sapi), + format!("{}", php_server_sapi), php_bin.clone(), args_str, ); - // Serialize PID content + std::thread::spawn(move || { + trace!("Starting healthcheck loop."); + let mut server_process = process; + loop { + std::thread::sleep(std::time::Duration::from_millis(100)); + let process_status = server_process.try_wait(); + match process_status { + Ok(Some(status)) => { + debug!("PHP stopped with exit code {}. Restarting it.", status.code().unwrap_or(255)); + let (_, mut command) = start_php_server(&php_bin, &php_server_port); + server_process = command.spawn().expect("Could not restart PHP server after failure."); + let pid = server_process.id().to_string(); + write(php_server_pid_file(), &pid).expect("Could not write PHP server PID to file after failure."); + debug!("PHP restarted, running with PID {}", pid); + }, + Ok(None) => (), // PHP server still alive. + Err(e) => panic!("An error occured when checking PHP server health: {:?}", e), + }; + } + }); + + // Serialize Server info let serialized = serde_json::to_string_pretty(&pid_info).unwrap(); - let path = get_rymfony_project_directory().unwrap(); - let server_pid_file = path.join("server.pid"); - let mut versions_file = File::create(&server_pid_file).unwrap(); - versions_file - .write_all(serialized.as_bytes()) + write(rymfony_server_info_file(), serialized.as_bytes()) .expect("Could not write PHP process information to cache file."); - php_server + (php_server_sapi, php_server_port) } -pub(crate) fn healthcheck(port: u16) -> u16 { - info!("Checking port {}", &port); +fn start_php_server(php_bin: &String, port: &u16) -> (PhpServerSapi, Command) { + let (sapi, command) = + if php_bin.contains("-fpm") && cfg!(not(target_family = "windows")) { + start_fpm(php_bin.clone(), port) + } else if php_bin.contains("-cgi") { + start_cgi(php_bin.clone(), port) + } else { + panic!("Rymfony only supports PHP-FPM (linux) and PHP-CGI (Windows), and none of these SAPIs was found."); + } + ; - 0 + (sapi, command) } diff --git a/src/php/server_cgi.rs b/src/php/server_cgi.rs index cc48528..c08c930 100644 --- a/src/php/server_cgi.rs +++ b/src/php/server_cgi.rs @@ -1,14 +1,10 @@ -use crate::php::php_server::PhpServer; use crate::php::structs::PhpServerSapi; use crate::utils::project_directory::get_rymfony_project_directory; use std::fs::{File, create_dir_all}; -use std::process::Child; use std::process::Command; use std::process::Stdio; -const CGI_DEFAULT_PORT: u16 = 65535; - -pub(crate) fn start(php_bin: String) -> (PhpServer, Child) { +pub(crate) fn start(php_bin: String, port: &u16) -> (PhpServerSapi, Command) { let mut command = Command::new(php_bin); let log_path = get_rymfony_project_directory().unwrap().join("log"); @@ -32,17 +28,17 @@ pub(crate) fn start(php_bin: String) -> (PhpServer, Child) { command .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) - .arg("-b") - .arg(format!("127.0.0.1:{}", CGI_DEFAULT_PORT)) - .arg("-d") + .arg("-b") // address:port + .arg(format!("127.0.0.1:{}", port.to_string())) + .arg("-d") // INI entries .arg(format!("error_log={}", error_log_file.to_str().unwrap())) - .arg("-e"); - - if let Ok(child) = command.spawn() { - info!("Running php-cgi with PID {}", child.id()); + .arg("-e") // extended information for debugger/profiler + ; - return (PhpServer::new(CGI_DEFAULT_PORT, PhpServerSapi::CGI), child); - } + // Strangely, php-cgi stops after this amount of requests, + // and it has no concurrency, so setting this to a high value + // avoids having to restart php-cgi too much. + command.env("PHP_FCGI_MAX_REQUESTS", "200000"); - panic!("Could not start php-cgi."); + (PhpServerSapi::CGI, command) } diff --git a/src/php/server_fpm.rs b/src/php/server_fpm.rs index f39147d..3756f83 100644 --- a/src/php/server_fpm.rs +++ b/src/php/server_fpm.rs @@ -9,25 +9,20 @@ use { std::fs::read_to_string, std::fs::remove_file, std::io::prelude::*, - std::process::Command, std::path::Path, std::process::Stdio, users::get_current_uid, - crate::php::structs::PhpServerSapi, - crate::utils::network::find_available_port, + crate::config::paths::php_fpm_conf_ini_file, crate::utils::project_directory::get_rymfony_project_directory }; -use crate::php::php_server::PhpServer; -use std::process::Child; +use crate::php::structs::PhpServerSapi; +use std::process::Command; // Possible values: alert, error, warning, notice, debug #[cfg(not(target_family = "windows"))] const FPM_DEFAULT_LOG_LEVEL: &str = "notice"; -#[cfg(not(target_family = "windows"))] -const FPM_DEFAULT_PORT: u16 = 60000; - // The placeholders between brackets {{ }} will be replaced with proper values. #[cfg(not(target_family = "windows"))] const FPM_DEFAULT_CONFIG: &str = " @@ -65,7 +60,7 @@ clear_env = no "; #[cfg(target_family = "windows")] -pub(crate) fn start(_php_bin: String) -> (PhpServer, Child) { +pub(crate) fn start(_php_bin: String, _port: &u16) -> (PhpServerSapi, Command) { panic!( "PHP-FPM does not exist on Windows.\ It seems the PHP version you selected is wrong.\ @@ -74,11 +69,9 @@ pub(crate) fn start(_php_bin: String) -> (PhpServer, Child) { } #[cfg(not(target_family = "windows"))] -pub(crate) fn start(php_bin: String) -> (PhpServer, Child) { +pub(crate) fn start(php_bin: String, port: &u16) -> (PhpServerSapi, Command) { let uid = get_current_uid(); - let mut port = find_available_port(FPM_DEFAULT_PORT); - // This is how you check whether systemd is active. // @see https://www.freedesktop.org/software/systemd/man/sd_booted.html let systemd_support = Path::new("/run/systemd/system/").exists(); @@ -92,7 +85,7 @@ pub(crate) fn start(php_bin: String) -> (PhpServer, Child) { .replace("{{ systemd_enable }}", if systemd_support { "" } else { ";" }) ; - let fpm_config_file_path = rymfony_project_path.join("fpm-conf.ini"); + let fpm_config_file_path = php_fpm_conf_ini_file(); if !fpm_config_file_path.exists() { let mut fpm_config_file = File::create(&fpm_config_file_path).unwrap(); @@ -102,24 +95,21 @@ pub(crate) fn start(php_bin: String) -> (PhpServer, Child) { debug!("Saved FPM config file at {}", fpm_config_file_path.to_str().unwrap()); } else { // Read the file and search the port - let mut content = read_to_string(&fpm_config_file_path).unwrap(); - - let port_used = read_port(&content).unwrap_or(port); - - let port_checked = find_available_port(port_used); - content = change_port(&content, &port_checked); - port = port_checked; - - remove_file(&fpm_config_file_path).expect("Could not remove php-fpm config file"); - let mut fpm_config_file = File::create(&fpm_config_file_path).unwrap(); - fpm_config_file.write_all(content.as_bytes()).expect( - format!( - "Could not write to php-fpm config file {}.", - &fpm_config_file_path.to_str().unwrap() - ) - .as_str(), - ); - debug!("Rewrote FPM config file at {}", fpm_config_file_path.to_str().unwrap()); + let content = read_to_string(&fpm_config_file_path).unwrap(); + + let port_used = read_port(&content).unwrap_or(port.clone()); + + if &port_used != port { + // If the port is different in the config file than in the current execution, + // we rewrite the whole config, but only changing the port. + let content = change_port(&content, &port); + remove_file(&fpm_config_file_path).expect("Could not remove php-fpm config file"); + let mut fpm_config_file = File::create(&fpm_config_file_path).unwrap(); + fpm_config_file + .write_all(content.as_bytes()) + .expect(format!("Could not write to php-fpm config file {}.", &fpm_config_file_path.to_str().unwrap()).as_str()); + debug!("Rewrote FPM config file at {}", fpm_config_file_path.to_str().unwrap()); + } } let fpm_log_file = OpenOptions::new() @@ -137,15 +127,11 @@ pub(crate) fn start(php_bin: String) -> (PhpServer, Child) { .unwrap() ; - let pid_filename = format!("{}/fpm.pid", rymfony_project_path.to_str().unwrap()); - let mut command = Command::new(php_bin); command .stdout(Stdio::from(fpm_log_file)) .stderr(Stdio::from(fpm_err_file)) .arg("--nodaemonize") - .arg("--pid") - .arg(pid_filename) .arg("--fpm-config") .arg(fpm_config_file_path.to_str().unwrap()); @@ -155,13 +141,7 @@ pub(crate) fn start(php_bin: String) -> (PhpServer, Child) { warn!("Be careful with permissions if your application has to manipulate the filesystem!") } - if let Ok(child) = command.spawn() { - info!("Running php-fpm with PID {}", child.id()); - - return (PhpServer::new(port, PhpServerSapi::FPM), child); - } - - panic!("Could not start php-fpm."); + (PhpServerSapi::FPM, command) } #[cfg(not(target_family = "windows"))] diff --git a/src/php/server_native.rs b/src/php/server_native.rs deleted file mode 100644 index 475de8b..0000000 --- a/src/php/server_native.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::php::php_server::PhpServer; -use crate::php::structs::PhpServerSapi; -use std::process::Child; -use std::process::Command; - -const NATIVE_DEFAULT_PORT: u16 = 65535; - -pub(crate) fn start(php_bin: String) -> (PhpServer, Child) { - let mut command = Command::new(php_bin); - - command - .arg("-S") - .arg(format!("127.0.0.1:{}", NATIVE_DEFAULT_PORT)); - - if let Ok(child) = command.spawn() { - info!("Running native PHP server with PID {}", child.id()); - - return ( - PhpServer::new(NATIVE_DEFAULT_PORT, PhpServerSapi::CLI), - child, - ); - } - - panic!("Could not start native PHP server."); -} diff --git a/src/php/structs.rs b/src/php/structs.rs index db00b95..8c14d98 100644 --- a/src/php/structs.rs +++ b/src/php/structs.rs @@ -12,7 +12,7 @@ use serde::Deserialize as SerdeDeserialize; use serde::Serialize; use serde::Serializer; -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub(crate) enum PhpServerSapi { FPM, CGI, @@ -255,7 +255,6 @@ impl PhpBinary { #[derive(Hash, Eq, PartialEq, Debug, Serialize, SerdeDeserialize)] pub(crate) struct ServerInfo { - pid: i32, port: u16, scheme: String, name: String, @@ -267,8 +266,7 @@ impl Display for ServerInfo { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { write!( f, - "pid: {}, port: {}, scheme: {}, name: {}, command: {}, args: {}", - self.pid, + "port: {}, scheme: {}, name: {}, command: {}, args: {}", self.port, self.scheme, self.name, @@ -280,7 +278,6 @@ impl Display for ServerInfo { impl ServerInfo { pub(crate) fn new( - pid: i32, port: u16, scheme: String, name: String, @@ -288,7 +285,6 @@ impl ServerInfo { args: Vec, ) -> ServerInfo { ServerInfo { - pid, port, scheme: scheme.clone(), name: name.clone(), @@ -296,9 +292,6 @@ impl ServerInfo { args: args.clone(), } } - pub(crate) fn pid(&self) -> i32 { - self.pid - } pub(crate) fn port(&self) -> u16 { self.port } diff --git a/src/utils/project_directory.rs b/src/utils/project_directory.rs index 40fbbbf..aef56e4 100644 --- a/src/utils/project_directory.rs +++ b/src/utils/project_directory.rs @@ -2,11 +2,16 @@ use std::env; use std::error::Error; use std::fmt; use std::fs::create_dir_all; +use std::fs::remove_file; use std::path::PathBuf; use std::result::Result; use dirs::home_dir; use sha2::Digest; +use crate::config::paths::php_server_pid_file; +use crate::config::paths::rymfony_pid_file; +use crate::config::paths::rymfony_server_info_file; +use crate::http::caddy::get_caddy_pid_path; #[derive(Debug)] struct ProjectDirectoryError(String); @@ -19,6 +24,13 @@ impl fmt::Display for ProjectDirectoryError { impl Error for ProjectDirectoryError {} +pub(crate) fn clean_rymfony_runtime_files() { + remove_file(rymfony_server_info_file()).unwrap_or_default(); + remove_file(rymfony_pid_file()).unwrap_or_default(); + remove_file(php_server_pid_file()).unwrap_or_default(); + remove_file(get_caddy_pid_path()).unwrap_or_default(); +} + pub(crate) fn get_rymfony_project_directory() -> Result> { let home = home_dir().unwrap().display().to_string(); let homestr = home.as_str();