1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use std::sync::Arc;

use crate::{
    config::Project,
    ext::{anyhow::Result, append_str_to_filename, determine_pdb_filename, fs},
    logger::GRAY,
    signal::{Interrupt, ReloadSignal, ServerRestart},
};
use camino::Utf8PathBuf;
use tokio::{
    process::{Child, Command},
    select,
    task::JoinHandle,
};

pub async fn spawn(proj: &Arc<Project>) -> JoinHandle<Result<()>> {
    let mut int = Interrupt::subscribe_shutdown();
    let proj = proj.clone();
    let mut change = ServerRestart::subscribe();
    tokio::spawn(async move {
        let mut server = ServerProcess::start_new(&proj).await?;
        loop {
            select! {
              res = change.recv() => {
                if let Ok(()) = res {
                      server.restart().await?;
                      ReloadSignal::send_full();
                }
              },
              _ = int.recv() => {
                    server.kill().await;
                    return Ok(())
              },
            }
        }
    })
}

struct ServerProcess {
    process: Option<Child>,
    envs: Vec<(&'static str, String)>,
    binary: Utf8PathBuf,
}

impl ServerProcess {
    fn new(proj: &Project) -> Self {
        Self {
            process: None,
            envs: proj.to_envs(),
            binary: proj.bin.exe_file.clone(),
        }
    }

    async fn start_new(proj: &Project) -> Result<Self> {
        let mut me = Self::new(proj);
        me.start().await?;
        Ok(me)
    }

    async fn kill(&mut self) {
        if let Some(proc) = self.process.as_mut() {
            if let Err(e) = proc.kill().await {
                log::error!("Serve error killing server process: {e}");
            } else {
                log::trace!("Serve stopped");
            }
            self.process = None;
        }
    }

    async fn restart(&mut self) -> Result<()> {
        self.kill().await;
        self.start().await?;
        log::trace!("Serve restarted");
        Ok(())
    }

    async fn start(&mut self) -> Result<()> {
        let bin = &self.binary;
        let child = if bin.exists() {
            // windows doesn't like to overwrite a running binary, so we copy it to a new name
            let bin_path = if cfg!(target_os = "windows") {
                // solution to allow cargo to overwrite a running binary on some platforms:
                //   copy cargo's output bin to [filename]_glory and then run it
                let new_bin_path = append_str_to_filename(bin, "_glory")?;
                log::debug!(
                    "Copying server binary {} to {}",
                    GRAY.paint(bin.as_str()),
                    GRAY.paint(new_bin_path.as_str())
                );
                fs::copy(bin, &new_bin_path).await?;
                // also copy the .pdb file if it exists to allow debugging to attach
                if let Some(pdb) = determine_pdb_filename(bin) {
                    let new_pdb_path = append_str_to_filename(&pdb, "_glory")?;
                    log::debug!(
                        "Copying server binary debug info {} to {}",
                        GRAY.paint(pdb.as_str()),
                        GRAY.paint(new_pdb_path.as_str())
                    );
                    fs::copy(&pdb, &new_pdb_path).await?;
                }
                new_bin_path
            } else {
                bin.clone()
            };

            log::debug!("Serve running {}", GRAY.paint(bin_path.as_str()));
            let cmd = Some(Command::new(bin_path).envs(self.envs.clone()).spawn()?);
            let port = self
                .envs
                .iter()
                .find_map(|(k, v)| if k == &"GLORY_SITE_ADDR" { Some(v.to_string()) } else { None })
                .unwrap_or_default();
            log::info!("Serving at http://{port}");
            cmd
        } else {
            log::debug!("Serve no exe found {}", GRAY.paint(bin.as_str()));
            None
        };
        self.process = child;
        Ok(())
    }
}