cloudpub_client/plugins/
httpd.rs

1use crate::config::{ClientConfig, EnvConfig};
2use crate::shell::{download, get_cache_dir, unzip, SubProcess, DOWNLOAD_SUBDIR};
3use crate::t;
4use anyhow::{Context, Result};
5use cloudpub_common::protocol::message::Message;
6use cloudpub_common::protocol::ServerEndpoint;
7use cloudpub_common::utils::find_free_tcp_port;
8use parking_lot::RwLock;
9use std::collections::HashMap;
10use std::sync::Arc;
11use tokio::sync::mpsc;
12
13#[cfg(unix)]
14use nix::sys::signal::{self, Signal};
15#[cfg(unix)]
16use nix::unistd::Pid;
17
18#[cfg(unix)]
19fn kill_by_pid_file(pid_file: &std::path::Path) {
20    use std::fs;
21    if !pid_file.exists() {
22        return;
23    }
24
25    let pid_str = match fs::read_to_string(pid_file) {
26        Ok(content) => content,
27        Err(e) => {
28            tracing::warn!("Failed to read PID file {}: {}", pid_file.display(), e);
29            return;
30        }
31    };
32
33    let pid_num: i32 = match pid_str.trim().parse() {
34        Ok(pid) => pid,
35        Err(e) => {
36            tracing::warn!(
37                "Failed to parse PID from file {}: {}",
38                pid_file.display(),
39                e
40            );
41            return;
42        }
43    };
44
45    let pid = Pid::from_raw(pid_num);
46
47    // Try to kill the process using nix crate
48    match signal::kill(pid, Signal::SIGTERM) {
49        Ok(()) => {
50            tracing::info!("Successfully sent SIGTERM to process {}", pid_num);
51        }
52        Err(nix::errno::Errno::ESRCH) => {
53            // Process doesn't exist, that's fine
54            tracing::debug!("Process {} not found (already dead)", pid_num);
55        }
56        Err(e) => {
57            tracing::warn!("Failed to kill process {}: {}", pid_num, e);
58        }
59    }
60
61    // Remove the PID file
62    if let Err(e) = fs::remove_file(pid_file) {
63        tracing::warn!("Failed to remove PID file {}: {}", pid_file.display(), e);
64    }
65}
66
67#[cfg(target_os = "windows")]
68pub const HTTPD_EXE: &str = "httpd.exe";
69#[cfg(unix)]
70pub const HTTPD_EXE: &str = "httpd";
71
72pub async fn setup_httpd(
73    config: &Arc<RwLock<ClientConfig>>,
74    command_rx: &mut mpsc::Receiver<Message>,
75    result_tx: &mpsc::Sender<Message>,
76    env: EnvConfig,
77) -> Result<()> {
78    let cache_dir = get_cache_dir(DOWNLOAD_SUBDIR)?;
79    let httpd_dir = get_cache_dir(&env.httpd_dir)?;
80
81    let mut touch = httpd_dir.clone();
82    touch.push("installed.txt");
83
84    if touch.exists() {
85        return Ok(());
86    }
87
88    let mut httpd = cache_dir.clone();
89    httpd.push(env.httpd.clone());
90
91    download(
92        &crate::t!("downloading-webserver"),
93        config.clone(),
94        format!("{}download/{}", config.read().server, env.httpd).as_str(),
95        &httpd,
96        command_rx,
97        result_tx,
98    )
99    .await
100    .context(crate::t!("error-downloading-webserver"))?;
101
102    unzip(
103        &crate::t!("unpacking-webserver"),
104        &httpd,
105        &httpd_dir,
106        1,
107        result_tx,
108    )
109    .await
110    .context(crate::t!("error-unpacking-webserver"))?;
111
112    #[cfg(target_os = "windows")]
113    {
114        use crate::shell::execute;
115        let mut redist = cache_dir.clone();
116        redist.push(env.redist.clone());
117
118        download(
119            &crate::t!("downloading-vcpp"),
120            config.clone(),
121            format!("{}download/{}", config.read().server, env.redist).as_str(),
122            &redist,
123            command_rx,
124            result_tx,
125        )
126        .await
127        .context(crate::t!("error-downloading-vcpp"))?;
128
129        if let Err(err) = execute(
130            redist,
131            vec![
132                "/install".to_string(),
133                "/quiet".to_string(),
134                "/norestart".to_string(),
135            ],
136            None,
137            Default::default(),
138            Some((crate::t!("installing-vcpp"), result_tx.clone(), 2)),
139            command_rx,
140        )
141        .await
142        {
143            // Non fatal error, probably components already installed
144            tracing::warn!("{}: {:?}", crate::t!("error-installing-vcpp"), err);
145        }
146    }
147    // Set exec mode for httpd_exe
148    #[cfg(unix)]
149    {
150        let httpd_exe = httpd_dir.join("bin").join(HTTPD_EXE);
151        use std::os::unix::fs::PermissionsExt;
152        std::fs::set_permissions(&httpd_exe, std::fs::Permissions::from_mode(0o755))
153            .context(crate::t!("error-setting-permissions"))?;
154    }
155
156    // Touch file to mark success
157    std::fs::write(touch, "Delete to reinstall").context(crate::t!("error-creating-marker"))?;
158
159    Ok(())
160}
161
162pub async fn start_httpd(
163    endpoint: &ServerEndpoint,
164    config_template: &str,
165    config_subdir: &str,
166    publish_dir: &str,
167    env: EnvConfig,
168    result_tx: mpsc::Sender<Message>,
169) -> Result<SubProcess> {
170    let httpd_dir = get_cache_dir(&env.httpd_dir)?;
171    let configs_dir = get_cache_dir(config_subdir)?;
172
173    let mut httpd_cfg = configs_dir.clone();
174    httpd_cfg.push(format!("{}.conf", endpoint.guid));
175
176    let mut pid_file = configs_dir.clone();
177    pid_file.push(format!("{}.pid", endpoint.guid));
178
179    let mut lock_file = configs_dir.clone();
180    lock_file.push(format!("{}.lock", endpoint.guid));
181
182    // Kill any existing httpd process before starting a new one
183    #[cfg(unix)]
184    kill_by_pid_file(&pid_file);
185
186    let port = find_free_tcp_port()
187        .await
188        .context(t!("error-finding-free-port"))?;
189
190    let httpd_config = config_template.replace("[[PUBLISH_DIR]]", publish_dir);
191    let httpd_config = httpd_config.replace("[[SRVROOT]]", httpd_dir.to_str().unwrap());
192    let httpd_config = httpd_config.replace("[[PORT]]", &port.to_string());
193    let httpd_config = httpd_config.replace("[[PID_FILE]]", pid_file.to_str().unwrap());
194    let httpd_config = httpd_config.replace("[[LOCK_FILE]]", lock_file.to_str().unwrap());
195
196    #[cfg(unix)]
197    let httpd_config = httpd_config.replace("[[IS_LINUX]]", "");
198
199    #[cfg(not(unix))]
200    let httpd_config = httpd_config.replace("[[IS_LINUX]]", "#");
201
202    std::fs::write(&httpd_cfg, httpd_config).context(crate::t!("error-writing-httpd-conf"))?;
203
204    let httpd_cfg = httpd_cfg.to_str().unwrap().to_string();
205    let httpd_exe = httpd_dir.join("bin").join(HTTPD_EXE);
206
207    #[allow(unused_mut)]
208    let mut envs = HashMap::<String, String>::new();
209
210    #[cfg(target_os = "macos")]
211    envs.insert(
212        "DYLD_LIBRARY_PATH".to_string(),
213        httpd_dir.join("lib").to_str().unwrap().to_string(),
214    );
215
216    #[cfg(target_os = "linux")]
217    envs.insert(
218        "LD_LIBRARY_PATH".to_string(),
219        httpd_dir.join("lib").to_str().unwrap().to_string(),
220    );
221
222    let server = SubProcess::new(
223        httpd_exe,
224        vec![
225            #[cfg(not(target_os = "windows"))]
226            "-X".to_string(),
227            "-f".to_string(),
228            httpd_cfg,
229        ],
230        None,
231        envs,
232        result_tx,
233        port,
234    );
235    Ok(server)
236}