Commit Diff


commit - /dev/null
commit + 0cde93542843a685bb3a38102ba74ac6820a7707
blob - /dev/null
blob + ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba (mode 644)
--- /dev/null
+++ .gitignore
@@ -0,0 +1 @@
+/target
blob - /dev/null
blob + eac36d50a0f7adf5814c3740e4970b064c1deabc (mode 644)
--- /dev/null
+++ .reuse/dep5
@@ -0,0 +1,8 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: d6
+Upstream-Contact: Dhruvin Gandhi <contact@dhruvin.dev>
+Source: https://git.dhruvin.dev/d6
+
+Files: *
+Copyright: 2023 Dhruvin Gandhi <contact@dhruvin.dev>
+License: ISC
blob - /dev/null
blob + 03c30204e975a72d93ec39db4cf6d34489d63722 (mode 644)
--- /dev/null
+++ Cargo.lock
@@ -0,0 +1,16 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "d6"
+version = "0.1.0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
blob - /dev/null
blob + efa9c2ac010962d15d3de19ac2808f0a82b9fe2b (mode 644)
--- /dev/null
+++ Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "d6"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+libc = "0.2.140"
blob - /dev/null
blob + b9c199c98f9bec183a195a9c0afc0a2e4fcc7654 (mode 644)
--- /dev/null
+++ LICENSES/ISC.txt
@@ -0,0 +1,8 @@
+ISC License:
+
+Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC")
+Copyright (c) 1995-2003 by Internet Software Consortium
+
+Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
blob - /dev/null
blob + bedf9bab838f771fa88a55ac654c4c638116536c (mode 644)
--- /dev/null
+++ src/bin/d6-metrics-exporter.rs
@@ -0,0 +1,86 @@
+use std::{
+    io::{self, BufWriter, Read, Write},
+    net::TcpStream,
+    os::fd::FromRawFd,
+    path::PathBuf,
+};
+
+use d6::{metrics, s6};
+
+#[derive(Debug)]
+enum Error {
+    MissingScanDir,
+    ExtraArgs,
+    Io(io::Error),
+}
+
+impl From<io::Error> for Error {
+    fn from(error: io::Error) -> Self {
+        Self::Io(error)
+    }
+}
+
+struct ChunkWriter<W: Write>(W);
+
+impl<W: Write> ChunkWriter<W> {
+    fn close(mut self) -> io::Result<()> {
+        self.0.write_all(b"0\r\n\r\n")?;
+        self.0.flush()
+    }
+}
+
+impl<W: Write> Write for ChunkWriter<W> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        let len = buf.len();
+        write!(self.0, "{len:X}\r\n")?;
+        self.0.write_all(buf)?;
+        self.0.write_all(b"\r\n")?;
+        Ok(len)
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.0.flush()
+    }
+}
+
+fn main() -> Result<(), Error> {
+    let mut args = std::env::args_os().skip(1);
+    let scan_dir = PathBuf::from(args.next().ok_or(Error::MissingScanDir)?);
+    if args.next().is_some() {
+        return Err(Error::ExtraArgs);
+    }
+    let mut stream = unsafe { TcpStream::from_raw_fd(0) };
+    stream.set_nonblocking(false)?;
+    let mut buf = [0; 1024];
+    let _ = stream.read(&mut buf)?;
+    if !buf.starts_with(b"GET /metrics HTTP/1.1\r\n") {
+        stream.write_all(
+            concat!(
+                "HTTP/1.1 400 Bad Request\r\n",
+                "Content-Length: 12\r\n",
+                "Connection: closed\r\n",
+                "\r\n",
+                "Bad Request\n"
+            )
+            .as_bytes(),
+        )?;
+        return Ok(());
+    }
+    stream.write_all(
+        concat!(
+            "HTTP/1.1 200 OK\r\n",
+            "Content-Type: application/openmetrics-text; version=1.0.0; charset=utf-8\r\n",
+            "Transfer-Encoding: chunked\r\n",
+            "\r\n"
+        )
+        .as_bytes(),
+    )?;
+    let chunk_writer = ChunkWriter(stream);
+    let mut writer = BufWriter::new(chunk_writer);
+    metrics::Writer::new(&mut writer, s6::ScanDir::new(scan_dir).service_dirs()?)
+        .collect::<Result<_, _>>()?;
+    match writer.into_inner() {
+        Err(e) => Err(Error::Io(e.into_error())),
+        Ok(chunk_writer) => Ok(chunk_writer.close()?),
+    }
+}
blob - /dev/null
blob + 0a608ee30f4fd380b5dddbae27e22e5bebee992a (mode 644)
--- /dev/null
+++ src/bin/d6-metrics.rs
@@ -0,0 +1,32 @@
+use std::{
+    io::{self, BufWriter, Write},
+    path::PathBuf,
+};
+
+use d6::{metrics, s6};
+
+#[derive(Debug)]
+enum Error {
+    MissingScanDir,
+    ExtraArgs,
+    Io(io::Error),
+}
+
+impl From<io::Error> for Error {
+    fn from(error: io::Error) -> Self {
+        Self::Io(error)
+    }
+}
+
+fn main() -> Result<(), Error> {
+    let mut args = std::env::args_os().skip(1);
+    let scan_dir = PathBuf::from(args.next().ok_or(Error::MissingScanDir)?);
+    if args.next().is_some() {
+        return Err(Error::ExtraArgs);
+    }
+    let mut writer = BufWriter::new(std::io::stdout().lock());
+    metrics::Writer::new(&mut writer, s6::ScanDir::new(scan_dir).service_dirs()?)
+        .collect::<Result<_, _>>()?;
+    writer.flush()?;
+    Ok(())
+}
blob - /dev/null
blob + d0490dd0d7296458eb055fdedc681d76dd95a5ff (mode 644)
--- /dev/null
+++ src/lib.rs
@@ -0,0 +1,2 @@
+pub mod metrics;
+pub mod s6;
blob - /dev/null
blob + 6aa8f990c300667b1b50dcb2a3b5dda67027071a (mode 644)
--- /dev/null
+++ src/metrics.rs
@@ -0,0 +1,106 @@
+use std::{
+    io::{Result, Write},
+    iter::FusedIterator,
+};
+
+use crate::s6::{ServiceDir, Status};
+
+pub struct Writer<'w, W: Write, I: Iterator<Item = ServiceDir>> {
+    writer: &'w mut W,
+    iter: I,
+    first: bool,
+    fused: bool,
+}
+
+impl<'w, W: Write, I: Iterator<Item = ServiceDir>> Writer<'w, W, I> {
+    pub fn new(writer: &'w mut W, iter: I) -> Self {
+        Self {
+            writer,
+            iter,
+            first: true,
+            fused: false,
+        }
+    }
+}
+
+impl<'w, W: Write, I: Iterator<Item = ServiceDir>> Iterator for Writer<'w, W, I> {
+    type Item = Result<()>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.fused {
+            return None;
+        }
+        self.iter
+            .next()
+            .map(|service_dir| {
+                let service = service_dir.name().to_string_lossy();
+                let status = service_dir.status()?;
+                if self.first {
+                    self.first = false;
+                    self.writer.write_all(
+                        concat!(
+                            "# TYPE s6_service_status stateset\n",
+                            "# HELP s6_service_status Status of the service.\n",
+                            "# TYPE s6_service_pid gauge\n",
+                            "# HELP s6_service_pid Process id of the service.\n",
+                            "# TYPE s6_service_exitcode stateset\n",
+                            "# HELP s6_service_exitcode Exit code of the service.\n",
+                            "# TYPE s6_service_signum stateset\n",
+                            "# HELP s6_service_signum Signal number of the service.\n",
+                        )
+                        .as_bytes(),
+                    )?;
+                }
+                match status {
+                    Status::Unsupervised => writeln!(
+                        self.writer,
+                        "s6_service_status{{service=\"{service}\",s6_service_status=\"supervised\"}} 0"
+                    ),
+                    Status::Supervised(supervised_status) => {
+                        write!(
+                            self.writer,
+                            concat!(
+                                "s6_service_status{{service=\"{}\",s6_service_status=\"supervised\"}} 1\n",
+                                "s6_service_status{{service=\"{0}\",s6_service_status=\"up\"}} {}\n",
+                                "s6_service_status{{service=\"{0}\",s6_service_status=\"wantedup\"}} {}\n",
+                                "s6_service_status{{service=\"{0}\",s6_service_status=\"ready\"}} {}\n",
+                                "s6_service_status{{service=\"{0}\",s6_service_status=\"paused\"}} {}\n",
+                                "s6_service_status{{service=\"{0}\",s6_service_status=\"normallyup\"}} {}\n",
+                                "s6_service_pid{{service=\"{0}\"}} {}\n",
+                            ),
+                            service,
+                            if supervised_status.up { 1 } else { 0 },
+                            if supervised_status.wantedup { 1 } else { 0 },
+                            if supervised_status.ready { 1 } else { 0 },
+                            if supervised_status.paused { 1 } else { 0 },
+                            if supervised_status.normallyup { 1 } else { 0 },
+                            supervised_status.pid,
+                        )?;
+                        if let Some(exitcode) = supervised_status.exitcode {
+                            writeln!(
+                                self.writer,
+                                concat!("s6_service_exitcode{{service=\"{}\",s6_service_exitcode=\"{}\"}} 1"),
+                                service,
+                                exitcode
+                            )?;
+                        }
+                        if let Some(signum) = supervised_status.signum {
+                            writeln!(
+                                self.writer,
+                                concat!("s6_service_signum{{service=\"{}\",s6_service_signum=\"{}\"}} 1"),
+                                service,
+                                signum
+                            )?;
+                        };
+                        Ok(())
+                    }
+                }
+            })
+            .or_else(|| {
+                self.fused = true;
+                Some(self.writer.write_all(b"# EOF\n"))
+            })
+    }
+}
+
+impl<'w, W: Write, I: Iterator<Item = ServiceDir>> FusedIterator for Writer<'w, W, I> {}
blob - /dev/null
blob + 3963632aa41bbc89dc4610558faa43664222cb35 (mode 644)
--- /dev/null
+++ src/s6.rs
@@ -0,0 +1,151 @@
+use std::{
+    ffi::OsStr,
+    fs::{File, OpenOptions},
+    io::{self, ErrorKind, Read},
+    os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
+    path::{Path, PathBuf},
+    ptr::addr_of_mut,
+};
+
+fn locked(f: &File) -> io::Result<bool> {
+    let mut flock = libc::flock {
+        l_type: libc::F_RDLCK as libc::c_short,
+        l_whence: libc::SEEK_SET as libc::c_short,
+        l_start: 0,
+        l_len: 0,
+        l_pid: 0,
+    };
+    match unsafe { libc::fcntl(f.as_raw_fd(), libc::F_GETLK, addr_of_mut!(flock)) } {
+        -1 => Err(io::Error::last_os_error()),
+        0 => Ok(flock.l_type != libc::F_UNLCK as libc::c_short),
+        _ => todo!("fcntl() != -1 && fcntl != 0"),
+    }
+}
+
+pub struct SvStatus {
+    pid: u64,
+    wstat: u16,
+    paused: bool,
+    finishing: bool,
+    wantup: bool,
+    ready: bool,
+}
+
+impl TryFrom<File> for SvStatus {
+    type Error = io::Error;
+
+    fn try_from(mut file: File) -> Result<Self, Self::Error> {
+        let mut pack = [0; 35];
+        file.read_exact(&mut pack)?;
+        Ok(Self {
+            pid: u64::from_be_bytes([
+                pack[24], pack[25], pack[26], pack[27], pack[28], pack[29], pack[30], pack[31],
+            ]),
+            wstat: u16::from_be_bytes([pack[32], pack[33]]),
+            paused: pack[34] & 1 == 1,
+            finishing: pack[34] & 2 == 2,
+            wantup: pack[34] & 4 == 4,
+            ready: pack[34] & 8 == 8,
+        })
+    }
+}
+
+pub struct SupervisedStatus {
+    pub up: bool,
+    pub wantedup: bool,
+    pub ready: bool,
+    pub paused: bool,
+    pub pid: u64,
+    pub normallyup: bool,
+    pub exitcode: Option<i32>,
+    pub signum: Option<i32>,
+    // tain, stamp, readystamp
+    // seconds, upseconds, readyseconds
+    // signal
+}
+
+pub enum Status {
+    Unsupervised,
+    Supervised(SupervisedStatus),
+}
+
+pub struct ServiceDir(PathBuf);
+
+impl ServiceDir {
+    pub fn name(&self) -> &OsStr {
+        self.0.file_name().unwrap()
+    }
+    pub fn supervised(&self) -> io::Result<bool> {
+        let mut path = self.0.clone();
+        path.extend(["supervise", "lock"]);
+        match OpenOptions::new()
+            .read(true)
+            .custom_flags(libc::O_NONBLOCK)
+            .open(&path)
+        {
+            Err(err) => match err.kind() {
+                ErrorKind::NotFound => Ok(false),
+                _ => Err(err),
+            },
+            Ok(lock) => locked(&lock),
+        }
+    }
+    pub fn status(&self) -> io::Result<Status> {
+        if self.supervised()? {
+            let mut path = self.0.clone();
+            path.extend(["supervise", "status"]);
+            let status = OpenOptions::new()
+                .read(true)
+                .custom_flags(libc::O_CLOEXEC)
+                .open(&path)
+                .and_then(SvStatus::try_from)?;
+            let wstat = status.wstat.into();
+            assert!(path.pop());
+            assert!(path.pop());
+            path.push("down");
+            let normallyup = !path.try_exists()?;
+            Ok(Status::Supervised(SupervisedStatus {
+                up: status.pid != 0 && !status.finishing,
+                wantedup: status.wantup,
+                ready: status.pid != 0 && status.ready,
+                paused: status.paused,
+                pid: status.pid,
+                exitcode: if status.pid != 0 && !status.finishing {
+                    None
+                } else if libc::WIFEXITED(wstat) {
+                    Some(libc::WEXITSTATUS(wstat))
+                } else {
+                    None
+                },
+                signum: if status.pid != 0 && !status.finishing {
+                    None
+                } else if libc::WIFSIGNALED(wstat) {
+                    Some(libc::WTERMSIG(wstat))
+                } else {
+                    None
+                },
+                normallyup,
+            }))
+        } else {
+            Ok(Status::Unsupervised)
+        }
+    }
+}
+
+pub struct ScanDir(PathBuf);
+
+impl ScanDir {
+    pub fn new<P: AsRef<Path>>(path: P) -> Self {
+        Self(PathBuf::from(path.as_ref()))
+    }
+    pub fn service_dirs(&self) -> io::Result<impl Iterator<Item = ServiceDir>> {
+        Ok(self.0.read_dir()?.flatten().filter_map(|entry| {
+            let path = entry.path();
+            if path.file_name().unwrap() == ".s6-svscan" {
+                None
+            } else {
+                Some(ServiceDir(path))
+            }
+        }))
+    }
+}