1use std::io::{Read, Write};
6use std::net::{Ipv4Addr, TcpListener, TcpStream};
7use std::path::{Path, PathBuf};
8use std::process::{Child, Command, Stdio};
9use std::time::Duration;
10
11use balise_core::error_codes::{DEV0004, DEV0005, DEV0006, DEV0007};
12use balise_core::router::HEALTH_PATHS;
13
14use crate::tools::{DOCKER, Machine};
15use crate::{Failure, project};
16
17const STOP_MARGIN: Duration = Duration::from_secs(5);
20
21const SERVICE: &str = "app";
23
24pub struct Compose {
26 root: PathBuf,
27 port: u16,
28 api_base: String,
29 variables: Vec<(String, String)>,
31}
32
33impl Compose {
34 pub fn new(root: &Path, machine: &dyn Machine, color: bool) -> Result<Compose, Failure> {
36 if !machine.installed(DOCKER.program) {
37 return Err(Failure::problem(DEV0004, &[], &[]));
38 }
39 if !machine.docker_running() {
40 return Err(Failure::problem(DEV0005, &[], &[]));
41 }
42 let config = project::config(root, &[]);
44 let grace = config.shutdown_timeout + STOP_MARGIN;
45 let mut variables = vec![
46 ("BALISE_VERSION", env!("CARGO_PKG_VERSION").to_string()),
47 ("BALISE_DEV_PORT", config.port.to_string()),
48 ("BALISE_DEV_STOP_GRACE", format!("{}ms", grace.as_millis())),
49 ("BALISE_DEV_USER", user()),
50 ];
51 if color {
52 variables.push(("CLICOLOR_FORCE", "1".to_string()));
53 }
54 Ok(Compose {
55 root: root.to_path_buf(),
56 port: config.port,
57 api_base: config.api_base(),
58 variables: variables
59 .into_iter()
60 .map(|(name, value)| (name.to_string(), value))
61 .collect(),
62 })
63 }
64
65 pub fn url(&self) -> String {
67 format!("http://localhost:{}{}", self.port, self.api_base)
68 }
69
70 pub fn running(&self, machine: &dyn Machine) -> Result<bool, Failure> {
72 let args = ["compose", "ps", "--status", "running", "--quiet", SERVICE];
73 let ids = machine
74 .output("docker", &args, &self.root, &self.variables)
75 .map_err(|cause| failed("ps", &cause))?;
76 Ok(!ids.trim().is_empty())
77 }
78
79 pub fn answers(&self) -> bool {
81 let Ok(mut stream) = TcpStream::connect((Ipv4Addr::LOCALHOST, self.port)) else {
82 return false;
83 };
84 let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
85 let request = format!(
86 "GET {} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
87 HEALTH_PATHS[0]
88 );
89 let mut response = String::new();
90 stream.write_all(request.as_bytes()).is_ok()
91 && stream.read_to_string(&mut response).is_ok()
92 && response.starts_with("HTTP/1.1 200")
93 }
94
95 pub fn up(&self, machine: &dyn Machine, detach: bool) -> Result<(), Failure> {
98 if !self.running(machine)? && TcpListener::bind((Ipv4Addr::LOCALHOST, self.port)).is_err() {
100 let other = self.port.saturating_add(1).to_string();
101 return Err(Failure::problem(
102 DEV0006,
103 &[("port", &self.port.to_string())],
104 &[("other", &other)],
105 ));
106 }
107 let args: &[&str] = match detach {
108 true => &["up", "--build", "--detach", "--wait"],
109 false => &["up", "--build", "--no-log-prefix"],
110 };
111 let _ = std::fs::create_dir_all(self.root.join("target"));
114 self.run(args)
115 }
116
117 pub fn down(&self) -> Result<(), Failure> {
120 self.run(&["down"])
121 }
122
123 fn run(&self, args: &[&str]) -> Result<(), Failure> {
126 super::catch_signals();
127 let mut command = Command::new("docker");
128 command
129 .arg("compose")
130 .args(args)
131 .current_dir(&self.root)
132 .envs(self.variables.iter().map(|(name, value)| (name, value)))
133 .stdin(Stdio::null());
134 #[cfg(unix)]
136 std::os::unix::process::CommandExt::process_group(&mut command, 0);
137 let mut child = command
138 .spawn()
139 .map_err(|error| failed(args[0], &error.to_string()))?;
140 let mut seen = 0;
141 let status = loop {
142 if let Some(status) = child
143 .try_wait()
144 .map_err(|e| failed(args[0], &e.to_string()))?
145 {
146 break status;
147 }
148 if let Some((received, signal)) = super::signal::received_since(seen) {
149 seen = received;
150 forward(&child, signal);
151 }
152 std::thread::sleep(Duration::from_millis(50));
153 };
154 match status.success() || super::stop_requested() {
155 true => Ok(()),
156 false => Err(failed(args[0], &super::app::describe(status))),
157 }
158 }
159}
160
161#[cfg(unix)]
162fn forward(child: &Child, signal: i32) {
163 unsafe {
165 libc::kill(child.id() as libc::pid_t, signal);
166 }
167}
168
169#[cfg(not(unix))]
170fn forward(_: &Child, _: i32) {
171 }
173
174fn failed(command: &str, cause: &str) -> Failure {
175 Failure::problem(DEV0007, &[("command", command), ("cause", cause)], &[])
176}
177
178#[cfg(unix)]
181fn user() -> String {
182 let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
184 format!("{uid}:{gid}")
185}
186
187#[cfg(not(unix))]
188fn user() -> String {
189 "root".to_string()
191}