Skip to main content

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
72/// The 1C and WebDAV plugins install Apache into the same cache directory.
73/// Concurrent setups (both plugins publishing on a cold cache, or duplicate
74/// endpoint starts) would unpack over each other and over a running httpd;
75/// serialize them and re-check the completion marker under the lock.
76static HTTPD_SETUP_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
77
78pub async fn setup_httpd(
79    config: &Arc<RwLock<ClientConfig>>,
80    command_rx: &mut mpsc::Receiver<Message>,
81    result_tx: &mpsc::Sender<Message>,
82    env: EnvConfig,
83) -> Result<()> {
84    let cache_dir = get_cache_dir(DOWNLOAD_SUBDIR)?;
85    let httpd_dir = get_cache_dir(&env.httpd_dir)?;
86
87    let mut touch = httpd_dir.clone();
88    touch.push("installed.txt");
89
90    let _setup_guard = HTTPD_SETUP_LOCK.lock().await;
91
92    if touch.exists() {
93        return Ok(());
94    }
95
96    let mut httpd = cache_dir.clone();
97    httpd.push(env.httpd.clone());
98
99    download(
100        &crate::t!("downloading-webserver"),
101        config.clone(),
102        format!("{}download/{}", config.read().server, env.httpd).as_str(),
103        &httpd,
104        command_rx,
105        result_tx,
106    )
107    .await
108    .context(crate::t!("error-downloading-webserver"))?;
109
110    unzip(
111        &crate::t!("unpacking-webserver"),
112        &httpd,
113        &httpd_dir,
114        1,
115        result_tx,
116    )
117    .await
118    .context(crate::t!("error-unpacking-webserver"))?;
119
120    #[cfg(target_os = "windows")]
121    {
122        use crate::shell::execute;
123        let mut redist = cache_dir.clone();
124        redist.push(env.redist.clone());
125
126        download(
127            &crate::t!("downloading-vcpp"),
128            config.clone(),
129            format!("{}download/{}", config.read().server, env.redist).as_str(),
130            &redist,
131            command_rx,
132            result_tx,
133        )
134        .await
135        .context(crate::t!("error-downloading-vcpp"))?;
136
137        if let Err(err) = execute(
138            redist,
139            vec![
140                "/install".to_string(),
141                "/quiet".to_string(),
142                "/norestart".to_string(),
143            ],
144            None,
145            Default::default(),
146            Some((crate::t!("installing-vcpp"), result_tx.clone(), 2)),
147            command_rx,
148        )
149        .await
150        {
151            // Non fatal error, probably components already installed
152            tracing::warn!("{}: {:?}", crate::t!("error-installing-vcpp"), err);
153        }
154    }
155    // Set exec mode for httpd_exe
156    #[cfg(unix)]
157    {
158        let httpd_exe = httpd_dir.join("bin").join(HTTPD_EXE);
159        use std::os::unix::fs::PermissionsExt;
160        std::fs::set_permissions(&httpd_exe, std::fs::Permissions::from_mode(0o755))
161            .context(crate::t!("error-setting-permissions"))?;
162    }
163
164    // Touch file to mark success
165    std::fs::write(touch, "Delete to reinstall").context(crate::t!("error-creating-marker"))?;
166
167    Ok(())
168}
169
170pub async fn start_httpd(
171    endpoint: &ServerEndpoint,
172    config_template: &str,
173    config_subdir: &str,
174    publish_dir: &str,
175    env: EnvConfig,
176    result_tx: mpsc::Sender<Message>,
177) -> Result<SubProcess> {
178    let httpd_dir = get_cache_dir(&env.httpd_dir)?;
179    let configs_dir = get_cache_dir(config_subdir)?;
180
181    let mut httpd_cfg = configs_dir.clone();
182    httpd_cfg.push(format!("{}.conf", endpoint.guid));
183
184    let mut pid_file = configs_dir.clone();
185    pid_file.push(format!("{}.pid", endpoint.guid));
186
187    let mut lock_file = configs_dir.clone();
188    lock_file.push(format!("{}.lock", endpoint.guid));
189
190    // Kill any existing httpd process before starting a new one
191    #[cfg(unix)]
192    kill_by_pid_file(&pid_file);
193
194    let port = find_free_tcp_port()
195        .await
196        .context(t!("error-finding-free-port"))?;
197
198    let httpd_config = config_template.replace("[[PUBLISH_DIR]]", publish_dir);
199    let httpd_config = httpd_config.replace("[[SRVROOT]]", httpd_dir.to_str().unwrap());
200    let httpd_config = httpd_config.replace("[[PORT]]", &port.to_string());
201    let httpd_config = httpd_config.replace("[[PID_FILE]]", pid_file.to_str().unwrap());
202    let httpd_config = httpd_config.replace("[[LOCK_FILE]]", lock_file.to_str().unwrap());
203
204    #[cfg(unix)]
205    let httpd_config = httpd_config.replace("[[IS_LINUX]]", "");
206
207    #[cfg(not(unix))]
208    let httpd_config = httpd_config.replace("[[IS_LINUX]]", "#");
209
210    std::fs::write(&httpd_cfg, httpd_config).context(crate::t!("error-writing-httpd-conf"))?;
211
212    let httpd_cfg = httpd_cfg.to_str().unwrap().to_string();
213    let httpd_exe = httpd_dir.join("bin").join(HTTPD_EXE);
214
215    #[allow(unused_mut)]
216    let mut envs = HashMap::<String, String>::new();
217
218    #[cfg(target_os = "macos")]
219    envs.insert(
220        "DYLD_LIBRARY_PATH".to_string(),
221        httpd_dir.join("lib").to_str().unwrap().to_string(),
222    );
223
224    #[cfg(target_os = "linux")]
225    envs.insert(
226        "LD_LIBRARY_PATH".to_string(),
227        httpd_dir.join("lib").to_str().unwrap().to_string(),
228    );
229
230    let server = SubProcess::new(
231        httpd_exe,
232        vec![
233            #[cfg(not(target_os = "windows"))]
234            "-X".to_string(),
235            "-f".to_string(),
236            httpd_cfg,
237        ],
238        None,
239        envs,
240        result_tx,
241        port,
242    );
243    Ok(server)
244}