Compare commits
7 commits
4ff0e65e91
...
548b22a55f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
548b22a55f | ||
|
|
21fc895b0e | ||
|
|
2ce7c3688c | ||
|
|
b6f1850a24 | ||
|
|
01d2c4f609 | ||
|
|
b161924bbc | ||
|
|
66ca52c187 |
3 changed files with 79 additions and 34 deletions
|
|
@ -124,22 +124,22 @@ fn follow_log(path: &str, tx: mpsc::Sender<String>) {
|
|||
}
|
||||
}
|
||||
|
||||
fn adjust_scroll(input: &str, cursor: usize, scroll: &mut usize, cols: usize) {
|
||||
let max_visible = cols.saturating_sub(2);
|
||||
fn byte_at(s: &str, char_idx: usize) -> usize {
|
||||
s.char_indices().nth(char_idx).map(|(i, _)| i).unwrap_or(s.len())
|
||||
}
|
||||
|
||||
fn adjust_scroll(_input: &str, cursor: usize, scroll: &mut usize, cols: usize) {
|
||||
let max_visible = cols.saturating_sub(3);
|
||||
if max_visible == 0 {
|
||||
return;
|
||||
}
|
||||
let threshold = (max_visible as f64 * 0.9) as usize;
|
||||
|
||||
if cursor > *scroll + threshold {
|
||||
*scroll = cursor - threshold;
|
||||
if cursor >= *scroll + max_visible {
|
||||
*scroll = cursor + 1 - max_visible;
|
||||
}
|
||||
if cursor < *scroll {
|
||||
*scroll = cursor;
|
||||
}
|
||||
if *scroll + max_visible > input.len() {
|
||||
*scroll = input.len().saturating_sub(max_visible);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
|
@ -177,19 +177,14 @@ fn draw_all(
|
|||
}
|
||||
|
||||
let prompt = "> ";
|
||||
let max_visible = cols.saturating_sub(prompt.len());
|
||||
let visible_start = scroll;
|
||||
let visible_end = (scroll + max_visible).min(input.len());
|
||||
let visible = if scroll < input.len() {
|
||||
&input[visible_start..visible_end]
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let max_visible = cols.saturating_sub(prompt.len() + 1);
|
||||
let visible: String = input.chars().skip(scroll).take(max_visible).collect();
|
||||
|
||||
out.push_str(&format!("\x1b[{};1H", rows));
|
||||
out.push_str(RESET);
|
||||
out.push_str(GRAY_BG);
|
||||
out.push_str(prompt);
|
||||
out.push_str(visible);
|
||||
out.push_str(&visible);
|
||||
out.push_str("\x1b[K");
|
||||
out.push_str(RESET);
|
||||
|
||||
|
|
@ -389,8 +384,8 @@ pub fn run_attach(name: &str, full: bool, stream: UnixStream) {
|
|||
}
|
||||
if let Some(idx) = history_idx {
|
||||
input = history[idx].clone();
|
||||
cursor = input.len();
|
||||
scroll = input.len().saturating_sub(cols.saturating_sub(2));
|
||||
cursor = input.chars().count();
|
||||
scroll = input.chars().count().saturating_sub(cols.saturating_sub(3));
|
||||
}
|
||||
adjust_scroll(&input, cursor, &mut scroll, cols);
|
||||
}
|
||||
|
|
@ -399,7 +394,7 @@ pub fn run_attach(name: &str, full: bool, stream: UnixStream) {
|
|||
if idx + 1 < history.len() {
|
||||
history_idx = Some(idx + 1);
|
||||
input = history[idx + 1].clone();
|
||||
cursor = input.len();
|
||||
cursor = input.chars().count();
|
||||
} else {
|
||||
history_idx = None;
|
||||
input.clear();
|
||||
|
|
@ -467,7 +462,10 @@ fn process_input_byte(
|
|||
0x08 | 0x7f => {
|
||||
if *cursor > 0 {
|
||||
*cursor -= 1;
|
||||
input.remove(*cursor);
|
||||
let byte_start = byte_at(input, *cursor);
|
||||
let c = input[byte_start..].chars().next().unwrap();
|
||||
let n = c.len_utf8();
|
||||
input.drain(byte_start..byte_start + n);
|
||||
}
|
||||
adjust_scroll(input, *cursor, scroll, cols);
|
||||
return InputAction::None;
|
||||
|
|
@ -478,7 +476,7 @@ fn process_input_byte(
|
|||
return InputAction::None;
|
||||
}
|
||||
0x05 => {
|
||||
*cursor = input.len();
|
||||
*cursor = input.chars().count();
|
||||
adjust_scroll(input, *cursor, scroll, cols);
|
||||
return InputAction::None;
|
||||
}
|
||||
|
|
@ -489,7 +487,7 @@ fn process_input_byte(
|
|||
return InputAction::None;
|
||||
}
|
||||
0x0b => {
|
||||
input.truncate(*cursor);
|
||||
input.truncate(byte_at(input, *cursor));
|
||||
return InputAction::None;
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -506,9 +504,9 @@ fn process_input_byte(
|
|||
if Some(utf8_buf.len()) == *utf8_expected {
|
||||
if let Ok(s) = std::str::from_utf8(&utf8_buf) {
|
||||
for c in s.chars() {
|
||||
let n = c.len_utf8();
|
||||
input.insert(*cursor, c);
|
||||
*cursor += n;
|
||||
let byte_pos = byte_at(input, *cursor);
|
||||
input.insert(byte_pos, c);
|
||||
*cursor += 1;
|
||||
}
|
||||
adjust_scroll(input, *cursor, scroll, cols);
|
||||
}
|
||||
|
|
@ -520,7 +518,8 @@ fn process_input_byte(
|
|||
*utf8_expected = None;
|
||||
}
|
||||
} else if b >= 0x20 {
|
||||
input.insert(*cursor, b as char);
|
||||
let byte_pos = byte_at(input, *cursor);
|
||||
input.insert(byte_pos, b as char);
|
||||
*cursor += 1;
|
||||
adjust_scroll(input, *cursor, scroll, cols);
|
||||
}
|
||||
|
|
@ -550,7 +549,7 @@ fn process_escape(
|
|||
}
|
||||
if let Some(idx) = *history_idx {
|
||||
*input = history[idx].clone();
|
||||
*cursor = input.len();
|
||||
*cursor = input.chars().count();
|
||||
}
|
||||
adjust_scroll(input, *cursor, scroll, cols);
|
||||
InputAction::None
|
||||
|
|
@ -561,7 +560,7 @@ fn process_escape(
|
|||
if idx + 1 < history.len() {
|
||||
*history_idx = Some(idx + 1);
|
||||
*input = history[idx + 1].clone();
|
||||
*cursor = input.len();
|
||||
*cursor = input.chars().count();
|
||||
} else {
|
||||
*history_idx = None;
|
||||
input.clear();
|
||||
|
|
@ -573,7 +572,8 @@ fn process_escape(
|
|||
}
|
||||
b'C' => {
|
||||
// Right arrow
|
||||
if *cursor < input.len() {
|
||||
let nchars = input.chars().count();
|
||||
if *cursor < nchars {
|
||||
*cursor += 1;
|
||||
}
|
||||
adjust_scroll(input, *cursor, scroll, cols);
|
||||
|
|
@ -593,7 +593,7 @@ fn process_escape(
|
|||
}
|
||||
b'F' => {
|
||||
// End
|
||||
*cursor = input.len();
|
||||
*cursor = input.chars().count();
|
||||
adjust_scroll(input, *cursor, scroll, cols);
|
||||
InputAction::None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,20 @@ pub fn run(config_path: String) {
|
|||
let _ = nix::sys::signal::sigaction(nix::sys::signal::Signal::SIGINT, &sig_action);
|
||||
}
|
||||
|
||||
// Single-instance guard: fail if another daemon already holds the lock.
|
||||
// This prevents two supervisors from double-starting the same services.
|
||||
let _daemon_lock = match state::lock_daemon() {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"error: another daemon is already running (lock {:?}): {}",
|
||||
state::daemon_pid_path(),
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Write daemon PID
|
||||
let pid_path = state::daemon_pid_path();
|
||||
if let Err(e) = state::write_pid(&pid_path) {
|
||||
|
|
@ -185,6 +199,9 @@ fn spawn(name: &str, cfg: &ServiceConfig) -> Option<Proc> {
|
|||
libc::_exit(1);
|
||||
}
|
||||
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM);
|
||||
// Put the service in its own process group so we can kill the
|
||||
// whole tree (sh + grandchildren) instead of leaking orphans.
|
||||
libc::setpgid(0, 0);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
|
@ -246,9 +263,10 @@ fn stop(name: &str, procs: &mut HashMap<String, Proc>) {
|
|||
let _ = Command::new("sh").arg("-c").arg(&cmd).output();
|
||||
}
|
||||
|
||||
// signal
|
||||
// signal the whole process group (pid negative) so sh and any
|
||||
// grandchildren all get it — otherwise background/pipeline children leak
|
||||
let sig = crate::supervisor::parse_signal(&p.cfg.stop_signal);
|
||||
let _ = nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid as i32), sig);
|
||||
let _ = nix::sys::signal::kill(nix::unistd::Pid::from_raw(-(pid as i32)), sig);
|
||||
|
||||
// wait
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(p.cfg.timeout_stop);
|
||||
|
|
@ -262,7 +280,7 @@ fn stop(name: &str, procs: &mut HashMap<String, Proc>) {
|
|||
// force kill
|
||||
if let Ok(None) = procs.get_mut(name).unwrap().child.try_wait() {
|
||||
let _ = nix::sys::signal::kill(
|
||||
nix::unistd::Pid::from_raw(pid as i32),
|
||||
nix::unistd::Pid::from_raw(-(pid as i32)),
|
||||
nix::sys::signal::Signal::SIGKILL,
|
||||
);
|
||||
let _ = procs.get_mut(name).unwrap().child.wait();
|
||||
|
|
@ -298,6 +316,13 @@ fn check_children(procs: &mut HashMap<String, Proc>) {
|
|||
|
||||
append_log(&log_path, &format!("O: exited with {}", code));
|
||||
|
||||
// Clean up anything still running in the dead service's process
|
||||
// group (e.g. background/pipeline children of `sh -c`).
|
||||
let _ = nix::sys::signal::kill(
|
||||
nix::unistd::Pid::from_raw(-(p.pid as i32)),
|
||||
nix::sys::signal::Signal::SIGKILL,
|
||||
);
|
||||
|
||||
if p.cfg.oneshot {
|
||||
dead.push(name.clone());
|
||||
continue;
|
||||
|
|
|
|||
20
src/state.rs
20
src/state.rs
|
|
@ -1,3 +1,4 @@
|
|||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn runtime_dir() -> PathBuf {
|
||||
|
|
@ -105,6 +106,25 @@ pub fn write_pid(path: &PathBuf) -> std::io::Result<()> {
|
|||
std::fs::write(path, format!("{}", std::process::id()))
|
||||
}
|
||||
|
||||
/// Acquire an exclusive lock on the daemon pid file.
|
||||
/// Held for the lifetime of the returned `File`; a second daemon fails here.
|
||||
pub fn lock_daemon() -> std::io::Result<std::fs::File> {
|
||||
let path = daemon_pid_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let f = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&path)?;
|
||||
let rc = unsafe { libc::flock(f.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
|
||||
if rc != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
pub fn is_enabled(name: &str) -> bool {
|
||||
enabled_path(name).exists()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue