# epics-tools-rs
Operational tooling for EPICS deployments — process supervisors, log/audit
dumpers, config validators.
First tenant: **`procserv`** — a pure-Rust port of
[`epics-modules/procServ`](https://github.com/epics-modules/procServ).
PTY-based process supervisor with a multi-client telnet console. Drop-in
flag compatibility with C `procServ`, so existing wrapper scripts and
systemd units do not need to be rewritten.
Unix-only (depends on `forkpty(3)` and POSIX signals). On non-Unix
targets the crate compiles to an empty library so workspace builds keep
succeeding; the `procserv-rs` binary is unavailable there.
## Build
From the workspace root:
```bash
cargo build -p epics-tools-rs --release --bin procserv-rs
# binary: target/release/procserv-rs
```
The default features (`procserv`, `procserv-bin`) already pull in the
binary; no extra `--features` flag is needed. To use the library only
without the CLI dependencies (`clap`, `tracing-subscriber`):
```toml
[dependencies]
epics-tools-rs = { version = "*", default-features = false, features = ["procserv"] }
```
## Run
`procserv-rs` mirrors the C `procServ` flag surface. Common shapes:
```bash
# Foreground, port 4051, supervising a softIoc
procserv-rs -f -p 4051 -- softIoc -d my.db
# Daemonized with log/pid/info files
procserv-rs -p 4051 \
--logfile /var/log/ioc.log \
--pidfile /var/run/ioc.pid \
--info-file /var/run/ioc.info \
--name myioc \
-- softIoc -d my.db
# Bind to all interfaces (default is localhost only)
procserv-rs -f -p 4051 --allow -- softIoc -d my.db
# UNIX domain socket instead of TCP
procserv-rs -f --unixpath /tmp/ioc.sock -- softIoc -d my.db
```
Everything after `--` is the child program and its argv.
### Flags
| `-p, --port <PORT>` | TCP listen port | — |
| `--allow` | Bind 0.0.0.0 instead of 127.0.0.1 | off |
| `--unixpath <PATH>` | UNIX-domain socket path | — |
| `-f, --foreground` | Do not daemonize | off |
| `-L, --logfile <PATH>` | Log file | — |
| `--pidfile <PATH>` | PID file | — |
| `--info-file <PATH>` | `PROCSERV_INFO` info file | — |
| `--holdoff <SEC>` | Hold-off between restarts | 15 |
| `-w, --wait` | Do not start child until first console request | off |
| `--chdir <DIR>` | `chdir` before exec'ing child | — |
| `--name <NAME>` | Display name in banners | child basename |
| `--max-restarts <N>` | Max restarts inside `--restart-window` | 10 |
| `--restart-window <SEC>` | Sliding window for `--max-restarts` | 600 |
| `--kill-char <BYTE>` | Force-kill key (Ctrl-X = 24, 0 disables) | 24 |
| `--toggle-restart-char <BYTE>` | Toggle restart-mode key (Ctrl-T = 20) | 20 |
| `--logout-char <BYTE>` | Per-client logout key (Ctrl-] = 29) | 29 |
### Connecting to the console
```bash
telnet localhost 4051
# or, for UNIX-socket listeners:
nc -U /tmp/ioc.sock
```
Multiple clients may connect simultaneously and share the child's stdout
in a party-line fashion. Built-in keys:
- `Ctrl-X` — force-kill the child (sends `SIGKILL` by default).
- `Ctrl-T` — toggle restart mode (`OnExit` ↔ off).
- `Ctrl-R` — manually restart the child when it is dead.
- `Ctrl-]` — log this client out (child stays running).
### Logging
`procserv-rs` uses the `tracing` ecosystem with an `EnvFilter`:
```bash
RUST_LOG=debug procserv-rs -f -p 4051 -- softIoc -d my.db
RUST_LOG=epics_tools_rs::procserv=trace procserv-rs ...
```
## Use as a library
```rust
use epics_tools_rs::procserv::{ProcServ, ProcServConfig};
# async fn run(cfg: ProcServConfig) -> Result<(), Box<dyn std::error::Error>> {
let server = ProcServ::new(cfg)?;
server.run().await?;
# Ok(()) }
```
The end-to-end test at `tests/procserv_e2e.rs` shows a complete config
literal with TCP listener, key bindings, restart policy and a
`/bin/cat` child — useful as a copy-paste starting point.
## Architecture
The C → Rust mapping is documented at the crate root in
[`src/lib.rs`](src/lib.rs). High-level points:
- Hub-and-spoke fan-out via a single supervisor task forwarding
per-connection mpsc messages, matching C `SendToAll`'s
exclude-the-sender semantics without `tokio::sync::broadcast`
re-delivery.
- Per-connection `readonly` flag instead of a master role.
- Stateless command-key dispatch (no menu FSM); keys are still echoed
to other connections.
- Narrow telnet usage — only `IAC WILL ECHO` and `IAC DO LINEMODE` are
negotiated, so the in-crate parser stays under ~80 LOC instead of
vendoring `libtelnet.c`.
## License
See [`LICENSE`](LICENSE).