commit 49d17a09aa9e27a2a6105da3a975dae0da589c5b Author: Luxferre Date: Sat Apr 4 13:11:12 2026 +0300 initial upload diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a8dbe42 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +# Makefile for DaemonTamer JPM build + +JC := jpm build --build-type=release --ldflags="-s" + +all: clean + $(JC) + +static: clean + $(JC) --janet-cflags="-static" + +clean: + jpm clean + +install: + jpm install diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb69f2e --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# DaemonTamer v3: a simple way to daemonize any user command + +This is the third iteration of the DaemonTamer project (hence DT), a simple and straightforward user-side daemon manager. DT's only purpose is allow the users to turn any shell command into a background-running +process by registering it in a special configuration file, and to manage it with a standard set of subcommands. + +Previous DT iterations were written in pure POSIX shell and experimentally rewritten in Python. The current version is fully hand-written from scratch in [Janet](https://janet-lang.org). + +## Functionality + +DT does: +- allow you to register any shell command to run in background, +- change the working directory according to the service configuration, +- run pre-start or post-stop hooks if they were specified, +- prevent the services from double-starting, +- allow you to specify custom paths for stdout and stderr logs, +- persist its service configuration in a JSON file (`$HOME/.dt.json`), +- provide you with the current status of any registered service or all at once. + +DT does **not**: +- supervise processes and/or apply any restart policy, +- elevate or drop privileges, +- provide any "autostart" capability upon the OS boot (it's up to the users to append corresponding `dt start` commands to their `.profile` or other autostart files). + +## Building and installation + +Besides Git and Make, you need to jave Janet, JPM and Spork installed as the dependencies. Refer to their official documentation and the documentation of your distro. + +Once ready, just run `make` or `make static` to build and then `sudo make install` (or `sudo jpm install`) to install. + +## Installation as a script + +Alternatively, if you have Janet and Spork installed but don't want to compile the binary, you can use DT as a script like this (feel free to use any other directory in your `$PATH`): + +``` +sudo cp dt.janet /usr/local/bin/dt +sudo chmod +x /usr/local/bin/dt +``` + +## Ready-made builds + +Static DT binary builds, if any, are going to be provided in the "Releases" section for Linux/x86_64 and Linux/ARM64 platforms only. + +## Usage + +### Registering a command as a service + +To register a command `command` under the service name `servicename`, run the following: + +``` +dt add servicename -c 'command' [-d workdir] [-o stdout-log-file] [-e stderr-log-file] [-b 'before-start-cmd'] [-a 'after-stop-cmd'] +``` + +The parameters are: + +``` + -a, --after-stop VALUE Post-stop hook command + -b, --before-start VALUE Pre-start hook command + -c, --command VALUE The command to run + -d, --directory VALUE Working directory + -e, --stderr-log VALUE=/dev/null Log file for stderr (default /dev/null) + -o, --stdout-log VALUE=/dev/null Log file for stdout (default /dev/null) +``` + +Note that you can overwrite/update an existing service entry by providing the same service name, so be careful. + +### Unregistering (deleting) a service + +Run `dt del servicename` and confirm your choice by entering `y`. + +### Getting a service status + +Run `dt status servicename` and see whether the service is running or stopped. + +### Listing all services with their statuses + +Run `dt list`. The columns will contain: service name, status, command, working directory. + +### Starting a service + +Run `dt start servicename`. If available, a before-start hook command will run first. + +### Stopping a service + +Run `dt stop servicename`. If available, an after-stop hook command will run after stopping. + +### Restarting a service + +Run `dt restart servicename`. Identical to `dt stop servicename && dt start servicename`. + +## Credits + +Created by Luxferre in 2026, released into the public domain with no warranties. + diff --git a/dt.janet b/dt.janet new file mode 100755 index 0000000..b5d1a25 --- /dev/null +++ b/dt.janet @@ -0,0 +1,158 @@ +#!/usr/bin/env janet +# DaemonTamer v3: a simple and portable way to daemonize any command, +# now ported to Janet (depends on spork) +# See README.md for (optional) compilation and usage +# Created by Luxferre in 2026, released into the public domain + +(import spork/json) +(import spork/path) +(import spork/sh) +(import spork/argparse :prefix "") + +(def script-path (-> (dyn :current-file) os/realpath path/dirname)) +(def config-path (string/format "%s/.dt.json" (os/getenv "HOME"))) +(def tempdir (sh/exec-slurp "/bin/sh" "-c" "dirname $(mktemp -u)")) +(def pid-file-template (string/format "%s/dt.%%s.pid" tempdir)) +(def nohup-template "nohup %s >>%s 2>>%s &\necho $! > %s") + +(defn- read-config [] + (if (os/stat config-path) (json/decode (slurp config-path)) @{})) + +(defn- write-config [cfg] + (spit config-path (json/encode cfg))) + +(defn- to-service-entry [params] + { + :command (get params "command") + :directory (or (get params "directory") script-path) + :before-start (get params "before-start") + :after-stop (get params "after-stop") + :stdout-log (or (get params "stdout-log") "/dev/null") + :stderr-log (or (get params "stderr-log") "/dev/null") + }) + +(defn- pid-file-path [sname] + (string/format pid-file-template sname)) + +(defn process-exists? [pid] + (default pid (os/getpid)) + (= 0 (os/execute ["kill" "-0" (string pid)] :p))) + +# Read a single-letter choice from the stdin +(defn get-choice [prompt] + (let [rawc (getline prompt)] + (if (> (length rawc) 0) + (string/ascii-lower (string/slice rawc 0 1)) + nil))) + +# Ask user for y/n confirmation (with custom prompt) +(defn user-confirm [prompt] + (var resp nil) + (var valid false) + (while (not valid) + (set resp (get-choice prompt)) + (set valid (string/check-set "yn" resp))) + (= resp "y")) + +# command handlers + +(defn- service-status [sname] + (let [pfile (pid-file-path sname)] + (if (os/stat pfile) (let [pid (scan-number (string/trim (slurp pfile)))] + (if (process-exists? pid) "running" "stopped")) + "stopped"))) + +(defn- start-service [sname entry] + (if (= (service-status sname) "stopped") (do + (printf "Starting service %s..." sname) + (let [ + workdir (entry :directory) + pre-start (get entry :before-start) + cmd (get entry :command) + sout (get entry :stdout-log) + serr (get entry :stderr-log) + ] + (os/cd workdir) + (when pre-start (os/shell pre-start)) + (os/shell (string/format nohup-template cmd sout serr (pid-file-path sname))) + (printf "Service %s started" sname))) + # already running service here + (eprintf "Service %s is already running!" sname))) + +(defn- stop-service [sname entry] + (printf "Stopping service %s..." sname) + (let [ + pfile (pid-file-path sname) + post-stop (get entry :after-stop) + ] + (when (os/stat pfile) (let [pid (scan-number (string/trim (slurp pfile)))] + (os/execute ["kill" (string pid)] :p) + (os/rm pfile) + (when post-stop (os/shell post-stop)) + (printf "Service %s stopped" sname) + )))) + +(defn- restart-service [sname entry] + (printf "Restarting service %s..." sname) + (stop-service sname entry) + (start-service sname entry) +) + +(defn process-action [action service-name params] + (let [cfg (read-config)] + (case action + "add" (let [service-entry (to-service-entry params)] + (put cfg service-name service-entry) + (write-config cfg) + (printf "Service %s registered" service-name) + ) + "del" (let [service-entry (get cfg service-name)] + (if service-entry + (when (user-confirm (string/format "Really delete the service %s? (y/n) " service-name)) + (put cfg service-name nil) + (write-config cfg) + (printf "Service %s unregistered" service-name)) + (eprint "This service name does not exist!"))) + "list" (eachp [sname entry] cfg + (printf "%s\t%s\t%s\t%s" sname + (service-status sname) + (entry "command") (entry "directory"))) + "status" (let [service-entry (get cfg service-name)] + (if service-entry + (printf "Service %s is %s." service-name (service-status service-name)) + (eprint "This service name does not exist!"))) + "start" (let [service-entry (to-service-entry (get cfg service-name))] + (if service-entry + (start-service service-name service-entry) + (eprint "This service name does not exist!"))) + "stop" (let [service-entry (to-service-entry (get cfg service-name))] + (if service-entry + (stop-service service-name service-entry) + (eprint "This service name does not exist!"))) + "restart" (let [service-entry (to-service-entry (get cfg service-name))] + (if service-entry + (restart-service service-name service-entry) + (eprint "This service name does not exist!"))) + (eprint "Invalid action! Allowed actions: add, del, start, stop, restart, list, status")))) + +(def cli-params + ["DaemonTamer v3 service manager (by Luxferre, 2026)" + :default {:kind :accumulate :required true :help "[action] [name]"} # positional params: action, name + "command" {:kind :option :short "c" :help "The command to run"} + "directory" {:kind :option :short "d" :help "Working directory"} + "stdout-log" {:kind :option :short "o" :help "Log file for stdout (default /dev/null)" :default "/dev/null"} + "stderr-log" {:kind :option :short "e" :help "Log file for stderr (default /dev/null)" :default "/dev/null"} + "before-start" {:kind :option :short "b" :help "Pre-start hook command"} + "after-stop" {:kind :option :short "a" :help "Post-stop hook command"}]) + +(defn main [&] + (let [args (argparse ;cli-params)] + (def posargs (get args :default)) + (if (and posargs (or (>= (length posargs) 2) (= (posargs 0) "list"))) + (do + (def [action service-name] (args :default)) + (process-action action service-name args) + ) (do + (eprint "Usage: dt {action} {service-name} [opts]\nAllowed actions: add, del, start, stop, restart, list, status") + (os/exit 1) + )))) diff --git a/project.janet b/project.janet new file mode 100644 index 0000000..95c22a8 --- /dev/null +++ b/project.janet @@ -0,0 +1,11 @@ +(declare-project + :name "DaemonTamer" + :description "A simple tool to daemonize any user command") + +(declare-source + :source ["dt.janet"]) + +(declare-executable + :name "dt" + :entry "dt.janet" + :install true)