use crate::earlier_unix_sockets_that_listen::{ListeningSockets, earlier_socket_ending, listening_sockets_ending_with};
use crate::wrapper_names_old_and_new::{COMMAND_WORD, DATA_FOLDER};
use std::path::{Path, PathBuf};
use std::time::Duration;
const SERVICE_MODE_WORDS: [&str; 2] = ["service", "run"];
const INSTANCE_OPTION: &str = "--instance";
const JOURNAL_OPTION: &str = "--journal";
pub const COMMAND_LINE_BYTES: u64 = 65_536;
const EARLIER_SOCKET_FOLDER: &str = DATA_FOLDER.earlier;
const EARLIER_SHARED_SOCKET_FILE: &str = "service.sock";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Hold {
InstanceSocketListens,
SharedSocketListens,
InstanceHelperRuns,
SharedHelperRuns,
HelperUnsure,
CannotLook,
}
impl Hold {
pub fn words(self) -> &'static str {
return match self {
Hold::InstanceSocketListens => "the socket of the earlier release's commit service for this worktree listens, and a connection to it starts that service at any moment",
Hold::SharedSocketListens => "the socket of the single commit service of the first releases listens (it serves every worktree), and a connection to it starts that service at any moment",
Hold::InstanceHelperRuns => "a program of the earlier release is running as the commit service of this worktree, and it finishes its own batches",
Hold::SharedHelperRuns => "a program of the first releases is running as the single commit service of every worktree, and it finishes its own batches",
Hold::HelperUnsure => "a running program has the earlier commit service's journal option with a folder this check could not resolve, so it may be that service",
Hold::CannotLook => "the sockets and processes of the earlier helper could not be looked at in time, so it is not known whether one runs",
};
}
}
#[derive(Clone, Debug)]
pub struct ServiceLook {
pub instance: String,
pub journals: Vec<PathBuf>,
pub own_pid: u32,
pub own_user: u32,
pub ceiling: Duration,
}
fn is_earlier_mode_at(words: &[&[u8]], at: usize) -> bool {
let Some(pair) = words.get(at..at + SERVICE_MODE_WORDS.len()) else {
return false;
};
let behind_current_word: bool = at > 0 && words[at - 1] == COMMAND_WORD.current.as_bytes();
return pair[0] == SERVICE_MODE_WORDS[0].as_bytes() && pair[1] == SERVICE_MODE_WORDS[1].as_bytes() && !behind_current_word;
}
fn is_word_end(byte: &u8) -> bool {
return *byte == 0;
}
#[cfg(unix)]
fn path_word(path: &Path) -> Vec<u8> {
use std::os::unix::ffi::OsStrExt;
return path.as_os_str().as_bytes().to_vec();
}
#[cfg(not(unix))]
fn path_word(path: &Path) -> Vec<u8> {
return path.to_string_lossy().into_owned().into_bytes();
}
#[cfg(unix)]
type FileIdentity = (u64, u64);
#[cfg(unix)]
struct JournalNames<'a> {
spellings: &'a [PathBuf],
process: &'a Path,
}
#[cfg(unix)]
fn journal_word_names(names: &JournalNames, word: &[u8]) -> Option<Hold> {
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
let hold_of = |index: usize| -> Hold { return if index == 1 { Hold::SharedHelperRuns } else { Hold::InstanceHelperRuns }; };
for (index, journal) in names.spellings.iter().enumerate() {
if path_word(journal.as_path()).as_slice() == word {
return Some(hold_of(index));
}
}
let spelled: &Path = Path::new(std::ffi::OsStr::from_bytes(word));
let resolved: PathBuf = if spelled.is_absolute() { spelled.to_path_buf() } else { names.process.join("cwd").join(spelled) };
let Ok(found) = std::fs::metadata(resolved.as_path()) else {
return Some(Hold::HelperUnsure);
};
for (index, journal) in names.spellings.iter().enumerate() {
if matches!(std::fs::metadata(journal), Ok(known) if (known.dev(), known.ino()) == (found.dev(), found.ino())) {
return Some(hold_of(index));
}
}
return None;
}
#[cfg(unix)]
fn is_service_of(words: &[&[u8]], look: &ServiceLook, names: &JournalNames) -> Option<Hold> {
let mode_in_a_row: bool = words.iter().enumerate().any(|(at, _)| { return is_earlier_mode_at(words, at); });
let mut instance_named: bool = false;
let mut journal_named: Option<Hold> = None;
for pair in words.windows(2) {
if pair[0] == INSTANCE_OPTION.as_bytes() && pair[1] == look.instance.as_bytes() {
instance_named = true;
}
if pair[0] == JOURNAL_OPTION.as_bytes()
&& journal_named.is_none()
&& let Some(named) = journal_word_names(names, pair[1])
{
journal_named = Some(named);
}
}
if journal_named.is_some() {
return journal_named;
}
return if mode_in_a_row && instance_named { Some(Hold::InstanceHelperRuns) } else { None };
}
#[cfg(unix)]
fn process_is_gone(error: &std::io::Error) -> bool {
return error.kind() == std::io::ErrorKind::NotFound || error.raw_os_error() == Some(libc::ESRCH);
}
#[cfg(unix)]
fn earlier_socket_that_listens(look: &ServiceLook, proc_root: &Path) -> Option<Hold> {
let shared: Vec<u8> = format!("/{EARLIER_SOCKET_FOLDER}/{EARLIER_SHARED_SOCKET_FILE}").into_bytes();
return match listening_sockets_ending_with(proc_root, &[earlier_socket_ending(look.instance.as_str())]) {
ListeningSockets::Listens => Some(Hold::InstanceSocketListens),
ListeningSockets::Unreadable => Some(Hold::CannotLook),
ListeningSockets::None => match listening_sockets_ending_with(proc_root, &[shared]) {
ListeningSockets::Listens => Some(Hold::SharedSocketListens),
ListeningSockets::Unreadable => Some(Hold::CannotLook),
ListeningSockets::None => None,
},
};
}
#[cfg(unix)]
enum CommandLine {
Words(Vec<u8>),
Skipped,
Unproven,
}
#[cfg(unix)]
fn command_line_of(entry: &std::fs::DirEntry, own_user: u32) -> CommandLine {
use std::io::Read;
use std::os::unix::fs::MetadataExt;
match entry.metadata() {
Ok(metadata) if metadata.uid() != own_user => return CommandLine::Skipped,
Ok(_) => {}
Err(error) if process_is_gone(&error) => return CommandLine::Skipped,
Err(_) => return CommandLine::Unproven,
}
let file = match std::fs::File::open(entry.path().join("cmdline")) {
Ok(file) => file,
Err(error) if process_is_gone(&error) => return CommandLine::Skipped,
Err(_) => return CommandLine::Unproven,
};
let mut bytes: Vec<u8> = Vec::new();
match file.take(COMMAND_LINE_BYTES + 1).read_to_end(&mut bytes) {
Ok(_) => {}
Err(error) if process_is_gone(&error) => return CommandLine::Skipped,
Err(_) => return CommandLine::Unproven,
}
if bytes.len() as u64 > COMMAND_LINE_BYTES {
return CommandLine::Unproven;
}
return CommandLine::Words(bytes);
}
#[cfg(unix)]
fn journal_identities(journals: &[PathBuf]) -> Option<Vec<FileIdentity>> {
use std::os::unix::fs::MetadataExt;
let mut identities: Vec<FileIdentity> = Vec::new();
for journal in journals {
match std::fs::metadata(journal) {
Ok(found) => identities.push((found.dev(), found.ino())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(_) => return None,
}
}
return Some(identities);
}
#[cfg(unix)]
fn scan(look: &ServiceLook, proc_root: &Path) -> Option<Hold> {
if let Some(hold) = earlier_socket_that_listens(look, proc_root) {
return Some(hold);
}
if journal_identities(look.journals.as_slice()).is_none() {
return Some(Hold::CannotLook);
}
let Ok(listing) = std::fs::read_dir(proc_root) else {
return Some(Hold::CannotLook);
};
for listed in listing {
let Ok(entry) = listed else {
return Some(Hold::CannotLook);
};
let name: String = entry.file_name().to_string_lossy().into_owned();
let Ok(pid) = name.parse::<u32>() else {
continue;
};
if pid == look.own_pid {
continue;
}
let bytes: Vec<u8> = match command_line_of(&entry, look.own_user) {
CommandLine::Words(bytes) => bytes,
CommandLine::Skipped => continue,
CommandLine::Unproven => return Some(Hold::CannotLook),
};
let process: PathBuf = entry.path();
let names: JournalNames = JournalNames { spellings: look.journals.as_slice(), process: process.as_path() };
let words: Vec<&[u8]> = bytes.strip_suffix(&[0]).unwrap_or(bytes.as_slice()).split(is_word_end).collect();
if let Some(hold) = is_service_of(words.as_slice(), look, &names) {
return Some(hold);
}
}
return None;
}
#[cfg(not(unix))]
fn scan(_look: &ServiceLook, _proc_root: &Path) -> Option<Hold> {
return Some(Hold::CannotLook);
}
#[cfg(unix)]
pub fn effective_user() -> u32 {
return unsafe { libc::geteuid() };
}
#[cfg(not(unix))]
pub fn effective_user() -> u32 {
return 0;
}
pub fn earlier_service_hold(look: ServiceLook, proc_root: &Path) -> Option<Hold> {
let ceiling: Duration = look.ceiling;
let root: PathBuf = proc_root.to_path_buf();
let (sender, receiver) = std::sync::mpsc::channel::<Option<Hold>>();
let started = std::thread::Builder::new().name(String::from("earlier-service-process-look")).spawn(move || {
let _sent = sender.send(scan(&look, root.as_path()));
});
if let Err(error) = started {
crate::service_error!("cannot start the look at the processes ({error}), so a service of the worktree may be running");
return Some(Hold::CannotLook);
}
return match receiver.recv_timeout(ceiling) {
Ok(hold) => hold,
Err(error) => {
crate::service_error!("the look at the processes gave no answer ({error}), so a service of the worktree may be running");
Some(Hold::CannotLook)
}
};
}
pub fn service_of_instance_may_run(look: ServiceLook, proc_root: &Path) -> bool {
return earlier_service_hold(look, proc_root).is_some();
}