├── README.md ├── circle.yml ├── daemon-runtime ├── project.clj └── src │ └── leiningen │ └── daemon │ └── runtime.clj ├── project.clj ├── src ├── bogus │ └── main.clj └── leiningen │ ├── daemon.clj │ ├── daemon │ └── common.clj │ ├── daemon_runtime.clj │ └── daemon_starter.clj └── test └── leiningen └── test_daemon.clj /README.md: -------------------------------------------------------------------------------- 1 | ** DEPRECATED ** 2 | 3 | I recommend using https://github.com/circleci/lein-jarbin instead. 4 | 5 | 6 | 7 | Lein-daemon is a lein plugin that starts a clojure process as a daemon. 8 | 9 | To use, add a :daemon option to your project.clj, it looks like 10 | 11 | ```clojure 12 | :daemon {:name-of-service {:ns my.name.space 13 | :pidfile "path-to-pidfile.pid"}} 14 | ``` 15 | 16 | The keys of the daemon map are the names of services that can be started at the command-line. Start a process with "lein daemon start :name-of-service". pidfile specifies where the pid file will be written. This path can be relative to the project directory, or absolute. 17 | 18 | If the pidfile is not specified, it defaults to name-of-service.pid, in the project's directory. 19 | 20 | Install 21 | ======= 22 | add lein-daemon to your leiningen project file, as a plugin:: 23 | 24 | ```clojure 25 | :plugins [[lein-daemon "0.5.4"]] 26 | ``` 27 | 28 | lein-daemon 0.5.0 and higher requires lein-2.0.0-RC1 or later. For lein1 support, use lein-daemon 0.4.x 29 | 30 | 31 | lein-daemon requires JNA to load the C standard library, so if you're using an uncommon JVM, you might need to install JNA on your box. If you're running Hotspot, you're probably fine. 32 | 33 | NS 34 | == 35 | Like `lein run`, `lein daemon` will call a function named `-main`, in the namespace specified by `:ns`. Any additional command line arguments to `lein daemon start foo` will be passed to -main. 36 | 37 | Operation 38 | ========= 39 | Lein daemon currently has three commands, `start`, `stop`, `check`. Start with `lein daemon start name-of-service`. This will call the -main function in the specified ns, with no arguments. Extra arguments may also be specified, like `lein daemon start service foo bar baz`. 40 | 41 | Stop the process with `lein daemon stop name-of-service`. Daemon will use the pid file to identify which process to stop. 42 | 43 | Check if the process is still running with `lein daemon check name-of-service`. 44 | 45 | 46 | Limitations / Assumptions 47 | =========== 48 | 49 | lein-daemon 0.5.0 and above require lein-2.0.0-RC1 or later. lein-daemon 0.4.x works on lein versions up to 1.6.1.1. There is no version that supports lein 1.7.x 50 | 51 | lein-daemon assumes you're on a unix-like system with `nohup` and `bash` installed. 52 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - lein2 test -------------------------------------------------------------------------------- /daemon-runtime/project.clj: -------------------------------------------------------------------------------- 1 | (defproject lein-daemon-runtime "0.5.0" 2 | :description "Runtime code for lein-daemon" 3 | :url "https://github.com/arohner/lein-daemon" 4 | :license {:name "Eclipse Public License"} 5 | :eval-in-leiningen true 6 | :dependencies [[com.sun.jna/jna "3.0.9"] 7 | [org.jruby.ext.posix/jna-posix "1.0.3"]] 8 | :dev-dependencies [[lein-clojars "0.9.1"] 9 | [org.clojure/clojure "1.4.0"]]) -------------------------------------------------------------------------------- /daemon-runtime/src/leiningen/daemon/runtime.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.daemon.runtime 2 | (:import java.io.FileOutputStream 3 | (org.jruby.ext.posix POSIXFactory 4 | POSIXHandler)) 5 | (:require [clojure.java.io :refer (reader)] 6 | [clojure.java.shell :as sh])) 7 | 8 | (defn throwf [& message] 9 | (throw (Exception. (apply format message)))) 10 | 11 | (def handler 12 | (proxy [POSIXHandler] 13 | [] 14 | (error [error extra] 15 | (println "error:" error extra)) 16 | (unimplementedError [methodname] 17 | (throwf "unimplemented method %s" methodname)) 18 | (warn [warn-id message & data] 19 | (println "warning:" warn-id message data)) 20 | (isVerbose [] 21 | false) 22 | (getCurrentWorkingDirectory [] 23 | (System/getProperty "user.dir")) 24 | (getEnv [] 25 | (map str (System/getenv))) 26 | (getInputStream [] 27 | System/in) 28 | (getOutputStream [] 29 | System/out) 30 | (getErrorStream [] 31 | System/err) 32 | (getPID [] 33 | (rand-int 65536)))) 34 | 35 | (def C (POSIXFactory/getPOSIX handler true)) 36 | 37 | (defn closeDescriptors [] 38 | (.close System/out) 39 | (.close System/err) 40 | (.close System/in)) 41 | 42 | (defn is-daemon? [] 43 | (System/getProperty "leiningen.daemon")) 44 | 45 | (defn chdirToRoot [] 46 | (.chdir C "/") 47 | (System/setProperty "user.dir" "/")) 48 | 49 | (defn get-current-pid [] 50 | (.getpid C)) 51 | 52 | (defn write-pid-file 53 | "Write the pid of the current process to pid-path" 54 | [pid-path] 55 | (let [pid (str (get-current-pid))] 56 | (printf "writing pid %s to %s\n" pid pid-path) 57 | (spit pid-path pid))) 58 | 59 | (defn abort 60 | "Abort, once we're in the user's project, so leiningen.core.main/abort isn't available" 61 | [message] 62 | (println message) 63 | (System/exit 1)) 64 | 65 | (defn init 66 | "do all the post-fork setup. set session id, close file descriptors, write pid file" 67 | [pid-path & {:keys [debug]}] 68 | ;; (.setsid C) 69 | ;; (when (not debug) 70 | ;; (closeDescriptors)) 71 | (write-pid-file pid-path)) 72 | 73 | (defn sigterm [pid] 74 | (.kill C pid 15)) 75 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject lein-daemon "0.5.5" 2 | :description "A lein plugin that daemonizes a clojure process" 3 | :url "https://github.com/arohner/leiningen" 4 | :license {:name "Eclipse Public License"} 5 | :eval-in-leiningen true 6 | :profiles {:dev 7 | {:dependencies 8 | [[bond "0.2.3" :exclusions [org.clojure/clojure]]]}}) 9 | -------------------------------------------------------------------------------- /src/bogus/main.clj: -------------------------------------------------------------------------------- 1 | (ns bogus.main 2 | "dummy namespace for testing") 3 | 4 | (defn -main [& args] 5 | (println "bogus.main/-main" args)) -------------------------------------------------------------------------------- /src/leiningen/daemon.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.daemon 2 | (:import java.io.File) 3 | (:require [clojure.string :as str] 4 | [clojure.java.shell :as sh] 5 | [leiningen.core.main :refer (abort)] 6 | [leiningen.core.eval :as eval] 7 | [leiningen.help :refer (help-for)] 8 | [leiningen.daemon.common :as common])) 9 | 10 | (defn wait-for 11 | "periodically calls test, a fn of no arguments, until it returns 12 | true, or timeout (in seconds) exceeded. Calls fail, a fn of no 13 | arguments if test never returns true" 14 | [test fail timeout] 15 | (let [start (System/currentTimeMillis) 16 | end (+ start (* timeout 1000))] 17 | (while (and (< (System/currentTimeMillis) end) (not (test))) 18 | (Thread/sleep 1)) 19 | (if (< (System/currentTimeMillis) end) 20 | true 21 | (fail)))) 22 | 23 | (defn get-pid 24 | "read and return the pid number contained in a pid-file, or nil" 25 | [path] 26 | (try 27 | (-> path (slurp) (Integer/parseInt)) 28 | (catch java.io.FileNotFoundException e 29 | nil) 30 | ;; this sometimes happens as a race, if the file has been created, 31 | ;; but the pid write hasn't fsync'd yet. 32 | (catch java.lang.NumberFormatException e 33 | nil))) 34 | 35 | (defn pid-present? 36 | "Returns the pid contained in the pidfile, if present, else nil" 37 | [project alias] 38 | (get-pid (common/get-pid-path project alias))) 39 | 40 | (defn running? 41 | "True if there's a process running with the pid contained in the pidfile" 42 | [project alias] 43 | (common/process-running? (pid-present? project alias))) 44 | 45 | (defn inconsistent? 46 | "true if pid is present, and process not running" 47 | [project alias] 48 | (and (pid-present? project alias) (not (running? project alias)))) 49 | 50 | (defn wait-for-running [project alias & {:keys [timeout] 51 | :or {timeout 300}}] 52 | (println "waiting for pid file to appear at" (common/get-pid-path project alias)) 53 | (wait-for #(running? project alias) 54 | #(common/throwf (format "%s failed to start in %s seconds" alias timeout)) timeout) 55 | (println alias "started")) 56 | 57 | (defn get-lein-script [] 58 | (System/getProperty "leiningen.script")) 59 | 60 | (defn do-start [project alias args] 61 | (let [timeout (* 5 60) 62 | arg-str (str/join " " args) 63 | alias (name alias) 64 | log-file (format "%s.log" (name alias)) 65 | lein (get-lein-script) 66 | nohup-cmd (format "nohup %s daemon-starter %s %s %s &" lein alias arg-str log-file)] 67 | (println "pid not present, starting") 68 | (when-not lein 69 | (abort "lein-daemon requires lein-2.0.0-RC1 or later")) 70 | (common/sh! "bash" "-c" nohup-cmd) 71 | (wait-for-running project alias))) 72 | 73 | (defn start-main 74 | [project alias args] 75 | (let [running? (running? project alias) 76 | pid-present? (pid-present? project alias)] 77 | (cond 78 | running? (abort "already running") 79 | pid-present? (abort "not starting, pid file present") 80 | :else (do-start project alias args)))) 81 | 82 | (defn delete-pid [project alias] 83 | (-> (common/get-pid-path project alias) (File.) (.delete))) 84 | 85 | (defn stop [project alias] 86 | (let [pid (get-pid (common/get-pid-path project alias)) 87 | timeout 60] 88 | (when (running? project alias) 89 | (println "sending SIGTERM to" pid) 90 | (common/sigterm pid)) 91 | (wait-for #(not (running? project alias)) #(common/throwf "%s failed to stop in %d seconds" alias timeout) timeout) 92 | (delete-pid project alias))) 93 | 94 | (defn check [project alias] 95 | (when (running? project alias) 96 | (do (println alias "is running") (System/exit 0))) 97 | (when (inconsistent? project alias) 98 | (do (println alias "pid present, but NOT running") (System/exit 2))) 99 | (do (println alias "is NOT running") (System/exit 1))) 100 | 101 | (defn abort-when-not [expr message & message-args] 102 | (when-not expr 103 | (abort (apply format message message-args)))) 104 | 105 | (declare usage) 106 | (defn ^{:help-arglists '([])} daemon 107 | "Run a -main function as a daemon, with optional command-line arguments. 108 | 109 | In project.clj, add a field pair that looks like 110 | :daemon {:foo {:ns foo.bar 111 | :pidfile \"foo.pid\"}} 112 | 113 | USAGE: lein daemon start foo 114 | USAGE: lein daemon stop foo 115 | USAGE: lein daemon check foo 116 | 117 | this will apply the -main method in foo.bar. 118 | 119 | On the start call, any additional arguments will be passed to -main 120 | 121 | USAGE: lein daemon start foo bar baz 122 | " 123 | [project & [command daemon-name & args :as all-args]] 124 | (when (or (nil? command) 125 | (nil? daemon-name)) 126 | (abort (help-for "daemon"))) 127 | (let [command (keyword command) 128 | daemon-name (common/get-daemon-name project daemon-name) 129 | daemon-args (get-in project [:daemon daemon-name :args]) 130 | args (concat daemon-args args)] 131 | (condp = (keyword command) 132 | :start (start-main project daemon-name args) 133 | :stop (stop project daemon-name) 134 | :check (check project daemon-name) 135 | (abort (str command " is not a valid command"))))) -------------------------------------------------------------------------------- /src/leiningen/daemon/common.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.daemon.common 2 | ;; this will be loaded by leiningen.daemon, so it can't have any dependencies not in lein 3 | (:require [clojure.java.shell :as sh])) 4 | 5 | (defn throwf [& message] 6 | (throw (Exception. (apply format message)))) 7 | 8 | (defn daemon-info-exists? [project daemon-name] 9 | (get-in project [:daemon daemon-name])) 10 | 11 | (defn default-pid-name [daemon-name] 12 | (format "%s.pid" (name daemon-name))) 13 | 14 | (defn get-pid-path [project daemon-name] 15 | (get-in project [:daemon daemon-name :pidfile] (default-pid-name daemon-name))) 16 | 17 | (defn get-daemon-name [project name] 18 | (cond 19 | (get-in project [:daemon name]) name 20 | (get-in project [:daemon (keyword name)]) (keyword name) 21 | :else (throw (Exception. (str "daemon " name " not found in :daemon section"))))) 22 | 23 | (defn debug? [project daemon-name] 24 | (get-in project [:daemon daemon-name :debug])) 25 | 26 | (defn sh! [& args] 27 | (let [resp (apply sh/sh args) 28 | exit-code (:exit resp)] 29 | (when (not (zero? exit-code)) 30 | (printf "%s returned %s: %s\n" args exit-code resp) 31 | (throwf "%s returned %s" args exit-code)) 32 | resp)) 33 | 34 | (defn ps [pid] 35 | (sh/sh "ps" (str pid))) 36 | 37 | (defn process-running? 38 | "returns true if the process with the specified PID is running" 39 | [pid] 40 | (-> (ps pid) :exit zero?)) 41 | 42 | (defn sigterm [pid] 43 | (sh/sh "kill" "-SIGTERM" (str pid))) -------------------------------------------------------------------------------- /src/leiningen/daemon_runtime.clj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arohner/lein-daemon/578a77a32b683fa05de07b77f4418b4e42c9ba0e/src/leiningen/daemon_runtime.clj -------------------------------------------------------------------------------- /src/leiningen/daemon_starter.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.daemon-starter 2 | (:require [leiningen.core.eval :as eval] 3 | [leiningen.daemon.common :as common])) 4 | 5 | (defn add-daemon-runtime-dependency 6 | [project] 7 | (if (some #(= 'lein-daemon-runtime (first %)) (:dependencies project)) 8 | project 9 | (update-in project [:dependencies] conj ['lein-daemon-runtime "0.5.0"]))) 10 | 11 | (defn daemon-starter [project & [daemon-name & args :as all-args]] 12 | (let [daemon-name (common/get-daemon-name project daemon-name) 13 | info (get-in project [:daemon daemon-name]) 14 | ns (symbol (:ns info)) 15 | pid-path (common/get-pid-path project daemon-name) 16 | debug? (common/debug? project daemon-name)] 17 | (eval/eval-in-project (add-daemon-runtime-dependency project) 18 | `(do 19 | (leiningen.daemon.runtime/init ~pid-path :debug ~debug?) 20 | (let [main-symbol# '~'-main 21 | main# (ns-resolve '~ns main-symbol#)] 22 | (when-not main# 23 | (leiningen.daemon.runtime/abort (format "%s/%s not found" '~ns main-symbol#))) 24 | (main# ~@args))) 25 | `(do 26 | (System/setProperty "leiningen.daemon" "true") 27 | (require '[leiningen.daemon.runtime]) 28 | (println "requiring" '~ns) 29 | (require '~ns))))) -------------------------------------------------------------------------------- /test/leiningen/test_daemon.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.test-daemon 2 | (:require [clojure.test :refer :all] 3 | [clojure.string :as str] 4 | [bond.james :as bond] 5 | [leiningen.core.main :refer (abort)] 6 | [leiningen.core.project :as project] 7 | [leiningen.daemon :as daemon] 8 | [leiningen.daemon-starter :as starter] 9 | [leiningen.daemon.common :as common])) 10 | 11 | (defn cleanup-pids [f] 12 | (common/sh! "bash" "-c" "rm -rf *.pid") 13 | (f)) 14 | 15 | (defn throw-on-abort [f] 16 | ;;redefine leiningen.core.main/abort to throw, rather than System/exit, so 17 | ;;our tests continue running! 18 | (with-redefs [abort (fn [msg#] 19 | (throw (Exception. msg#)))] 20 | (f))) 21 | 22 | (defn standard-lein-name [f] 23 | (with-redefs [daemon/get-lein-script (constantly "lein")] 24 | (f))) 25 | 26 | (defmacro with-no-spawn [& body] 27 | `(bond/with-stub [common/sh! daemon/wait-for-running] 28 | ~@body)) 29 | 30 | (use-fixtures :each cleanup-pids throw-on-abort standard-lein-name) 31 | 32 | (deftest daemon-args-are-passed-to-do-start 33 | (with-no-spawn 34 | (let [project {:daemon {"foo" {:pidfile "foo.pid" 35 | :args ["bar" "baz"]}}}] 36 | (daemon/daemon project "start" "foo") 37 | (let [bash-cmd (str/join " " (-> common/sh! bond/calls first :args))] 38 | (is (re-find #"lein daemon-starter foo bar baz" bash-cmd)))))) 39 | 40 | (deftest cmd-line-args-are-passed-to-do-start 41 | (with-no-spawn 42 | (let [project {:daemon {"foo" {:pidfile "foo.pid"}}}] 43 | (daemon/daemon project "start" "foo" "bar") 44 | (let [bash-cmd (str/join " " (-> common/sh! bond/calls first :args))] 45 | (is (re-find #"lein daemon-starter foo bar" bash-cmd)))))) 46 | 47 | (deftest daemon-cmd-line-args-are-combined 48 | (with-no-spawn 49 | (let [project {:daemon {"foo" {:pidfile "foo.pid" 50 | :args ["bar"]}}}] 51 | (daemon/daemon project "start" "foo" "baz") 52 | (let [bash-cmd (str/join " " (-> common/sh! bond/calls first :args))] 53 | (is (re-find #"lein daemon-starter foo bar baz" bash-cmd)))))) 54 | 55 | (deftest passing-string-foo-on-cmd-line-finds-keyword-foo 56 | (with-no-spawn 57 | (let [project {:daemon {:foo {:ns "foo.bar"}}}] 58 | (daemon/daemon project "start" "foo") 59 | (let [bash-cmd (str/join " " (-> common/sh! bond/calls first :args))] 60 | (is (re-find #"lein daemon-starter foo" bash-cmd)))))) 61 | 62 | (deftest passing-string-foo-on-cmd-line-finds-string-foo 63 | (with-no-spawn 64 | (let [project {:daemon {"foo" {:ns "foo.bar"}}}] 65 | (daemon/daemon project "start" "foo") 66 | (let [bash-cmd (str/join " " (-> common/sh! bond/calls first :args))] 67 | (is (re-find #"lein daemon-starter foo" bash-cmd)))))) 68 | 69 | (def dummy-project (project/make {:eval-in :subprocess 70 | :dependencies ['[org.clojure/clojure "1.4.0"]]})) 71 | 72 | (deftest pid-path-handles-keywords 73 | (let [project (merge dummy-project {:daemon {:foo {:ns "bogus.main"}}})] 74 | (is (= "foo.pid" (common/get-pid-path project :foo))))) 75 | 76 | (deftest daemon-starter-finds-string-info 77 | (starter/daemon-starter (merge dummy-project {:daemon {"foo" {:ns "bogus.main"}}}) "foo")) 78 | 79 | (deftest daemon-starter-finds-keyword-daemon 80 | (starter/daemon-starter (merge dummy-project {:daemon {:foo {:ns "bogus.main"}}}) "foo")) 81 | 82 | (deftest log-files-dont-include-colon 83 | (with-no-spawn 84 | (with-no-spawn 85 | (let [project {:daemon {:foo {:ns "foo.bar"}}}] 86 | (daemon/daemon project "start" "foo") 87 | (let [bash-cmd (str/join " " (-> common/sh! bond/calls first :args))] 88 | (is (re-find #"> foo.log" bash-cmd))))))) --------------------------------------------------------------------------------