luhproc

Crates.ioluhproc
lib.rsluhproc
version0.1.2
created_at2025-12-07 01:29:55.392883+00
updated_at2025-12-07 01:32:42.114172+00
descriptionA lightweight background process manager
homepage
repositoryhttps://github.com/calizoots/luhtwin
max_upload_size
id1970997
size76,162
s (calizoots)

documentation

README

luhproc

github crates.io docs.rs

A lightweight background process manager

made with love s.c

luhproc provides a simple system for spawning, tracking, and stopping background worker processes in Rust applications.

It is built around two core ideas:

  • Workers are identified by environment variables (e.g. MY_TASK=1)
  • Workers run in background mode when started from the same binary

This makes it easy to run small persistent tasks (indexers, watchers, refreshers, schedulers, etc.) without needing systemd, Docker, or any external supervisor.

Quick Start

Add luhtwin to your Cargo.toml:

[dependencies]
luhproc = "0.1"

Example


static PM: OnceLock<ProcessManager> = OnceLock::new();

fn child_work() -> LuhTwin<()> {
    info!("hello from the child thread");
    Ok(())
}

fn main() -> LuhTwin<() {
    PM.set(process_manager!("MOTHAPP" => start_moth)
           .encase(|| "failed to make process_manager")?)
        .unwrap();

    PM.get().unwrap().check()
        .encase(|| "failed to run child process")?;

    PM.get().unwrap().start("MOTHAPP", "app", None)
    .encase(|| "failed to start moth app")?;
}

Basic Concept

You define tasks using the [process_manager!] macro:

process_manager! {
    "MY_TASK" => my_background_function,
    "REFRESH_CACHE" => refresh_cache_worker,
};

A task is triggered when the binary starts with that environment variable set.

For example:

MY_TASK=1 ./myapp

The main process will immediately run the function associated with the task, then exit after finishing.

The task can be launched in the background through the API:

pm.start("MY_TASK", "unique-id", None)?;

Each running worker is stored inside a temporary directory containing:

<tmp>/luhproc/my-task-<hash>/
├── out.log   # stdout
├── err.log   # stderr
└── pid       # process ID

Architecture

ChildTask

Represents one runnable background task.

pub struct ChildTask {
    pub id: String,
    pub env_var: &'static str,
    pub work: fn() -> LuhTwin<()>,
}
  • id — unique instance identifier (affects directory hashing)
  • env_var — environment variable that triggers the worker
  • work — the task function itself

Generated Directory Name

Each task instance gets its own hashed directory:

my-task-3fa92k19cd12

This allows multiple instances of the same task type.


ProcessManager

Main API for controlling worker tasks.

pub struct ProcessManager {
    pub tasks: Vec<ChildTask>,
}

Registering Tasks

let mut pm = ProcessManager::new()?;
pm.register_task("MY_TASK", my_worker_fn);

Or with the macro:

let pm = process_manager! {
    "MY_TASK" => my_worker_fn,
    "SYNC" => sync_worker_fn,
}?;

Starting a Worker

Spawns a background process by re-invoking the binary with an env var:

pm.start("MY_TASK", "session42", None)?;

Internally:

  • creates temp directory
  • forks current executable
  • writes PID file
  • redirects stdout/stderr

Stopping a Worker

pm.stop("MY_TASK", Some("session42"))?;

Or stop all workers of that type:

pm.stop("MY_TASK", None)?;

Checking Worker Status

let details = pm.info("MY_TASK", "session42")?;
println!("{details}");

Example output:

task: MY_TASK (id: session42)
pid: 39241
directory: /tmp/luhproc/my-task-a8fd93c2e1a3
log file: /tmp/luhproc/.../out.log
error file: /tmp/luhproc/.../err.log

File Layout

/tmp/luhproc/
/
/my-task-ae92f139ab23/
/    pid        # process ID
/    out.log    # stdout of worker
/    err.log    # stderr of worker

If LUHPROC_TMP_DIR is set, that directory is used instead of /tmp or dev temp.


Environment Trigger Check

At the start of your application, call:

pm.check()?;

If the process was started as a worker, the task runs inline and the parent process exits afterwards.

Your main typically looks like:

fn main() -> LuhTwin<()> {
    let pm = process_manager! {
        "MY_TASK" => run_worker,
    }?;

    // check for background-worker mode
    pm.check()?;

    // normal application logic
    run_cli()?;
    Ok(())
}

Behavior Notes

  • Workers are single-process, not threadpools.
  • Stopping uses SIGTERM (Unix-only via nix).
  • If a PID file becomes invalid, stop() will report an error but still clean up.
  • If your worker loops forever, ensure it handles SIGTERM gracefully.

Example Worker

fn ping_worker() -> LuhTwin<()> {
    loop {
        println!("ping!");
        std::thread::sleep(std::time::Duration::from_secs(1));
    }
}

And launching it:

let pm = process_manager! {
    "PING_WORKER" => ping_worker,
}?;

pm.check()?;


pm.start("PING_WORKER", "main", None)?;
Commit count: 0

cargo fmt