Skip to content

Commit 95c22dc

Browse files
committed
Add facilities for running embedded processes
1 parent 4956781 commit 95c22dc

File tree

2 files changed

+104
-38
lines changed

2 files changed

+104
-38
lines changed

CHANGELOG.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
## Added
44

5-
## Fixed
6-
7-
## Changed
5+
- Allow setting :java-args, :middleware, :eval-forms directly from the top-level opts
6+
- Add facilities for running nested processes, `lauchpad/run-process`
87

98
# 0.21.106-alpha (2024-01-10 / 8fc8faf)
109

@@ -162,4 +161,4 @@ Initial release
162161
- lambdaisland.classpath integration
163162
- Support for cider-nrepl, refactor-nrepl
164163
- Basic support for shadow-cljs cljs nREPL-base REPL
165-
- Auto-connect for Emacs
164+
- Auto-connect for Emacs

src/lambdaisland/launchpad.clj

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
(ns lambdaisland.launchpad
2-
(:require [babashka.curl :as curl]
3-
[babashka.process :refer [process]]
4-
[babashka.wait :as wait]
5-
[cheshire.core :as json]
6-
[clojure.core.async :as async]
7-
[clojure.edn :as edn]
8-
[clojure.java.io :as io]
9-
[clojure.java.shell :as shell]
10-
[clojure.pprint :as pprint]
11-
[clojure.string :as str]
12-
[clojure.tools.cli :as tools-cli]
13-
[lambdaisland.dotenv :as dotenv])
14-
(:import java.net.ServerSocket))
2+
(:require
3+
[babashka.process :refer [process]]
4+
[babashka.wait :as wait]
5+
[clojure.edn :as edn]
6+
[clojure.java.io :as io]
7+
[clojure.java.shell :as shell]
8+
[clojure.pprint :as pprint]
9+
[clojure.string :as str]
10+
[clojure.tools.cli :as tools-cli]
11+
[lambdaisland.dotenv :as dotenv])
12+
(:import
13+
(java.io InputStream OutputStream)
14+
(java.lang Process ProcessBuilder)
15+
(java.net ServerSocket)
16+
(java.util.concurrent TimeUnit)))
1517

1618
(def cli-opts
1719
[["-h" "--help"]
@@ -122,15 +124,18 @@
122124
(read-string
123125
(eval-emacs 'cljr-injected-middleware-version))))
124126

127+
(defn add-nrepl-middleware [& mws]
128+
(fn [ctx]
129+
(update ctx :middleware (fnil into []) mws)))
130+
125131
(defn compute-middleware
126132
"Figure out the nREPL middleware based on CLI flags"
127133
[{:keys [options] :as ctx}]
128-
(let [add-mw #(update %1 :middleware (fnil conj []) %2)]
129-
(cond-> ctx
130-
(:cider-nrepl options)
131-
(add-mw 'cider.nrepl/cider-middleware)
132-
(:refactor-nrepl options)
133-
(add-mw 'refactor-nrepl.middleware/wrap-refactor))))
134+
(cond-> ctx
135+
(:cider-nrepl options)
136+
((add-nrepl-middleware 'cider.nrepl/cider-middleware))
137+
(:refactor-nrepl options)
138+
((add-nrepl-middleware 'refactor-nrepl.middleware/wrap-refactor))))
134139

135140
(defn compute-extra-deps [{:keys [options] :as ctx}]
136141
(let [assoc-dep #(update %1 :extra-deps assoc %2 %3)]
@@ -353,12 +358,6 @@
353358
(defn include-launchpad-deps [{:keys [extra-deps] :as ctx}]
354359
(update ctx :extra-deps assoc 'com.lambdaisland/launchpad (find-launchpad-coords)))
355360

356-
(defn start-process [{:keys [options aliases nrepl-port env] :as ctx}]
357-
(let [args (clojure-cli-args ctx)]
358-
(apply debug (map shellquote args))
359-
(let [process (process args {:env env :out :inherit :err :inherit})]
360-
(assoc ctx :clojure-process process))))
361-
362361
(defn maybe-connect-emacs [{:keys [options nrepl-port project-root] :as ctx}]
363362
(when (:cider-connect options)
364363
(debug "Connecting CIDER with project-dir" project-root)
@@ -398,6 +397,62 @@
398397
(:extra-deps ctx)))
399398
ctx)
400399

400+
(defn pipe-process-output
401+
"Prefix output from a process with, prefixing it"
402+
[^java.lang.Process proc prefix]
403+
(let [out (.getInputStream proc)
404+
err (.getErrorStream proc)
405+
newline? (volatile! true)
406+
^bytes buffer (make-array Byte/TYPE 1024)]
407+
(doseq [[^InputStream from ^OutputStream to] [[out System/out] [err System/err]]]
408+
(future
409+
(loop []
410+
(let [size (.read from buffer)]
411+
(when (pos? size)
412+
(dotimes [i size]
413+
(when @newline?
414+
(.write to (.getBytes prefix))
415+
(vreset! newline? false))
416+
(let [b (aget buffer i)]
417+
(.write to (int b))
418+
(when (= (int \newline) b)
419+
(vreset! newline? true))))))
420+
(Thread/sleep 100)
421+
(recur))))
422+
proc))
423+
424+
(defn run-process [{:keys [cmd prefix working-dir
425+
background? timeout-ms check-exit-code? env
426+
color]
427+
:or {working-dir "."
428+
check-exit-code? true}}]
429+
(fn [ctx]
430+
(let [working-dir (io/file working-dir)
431+
proc-builder (doto (ProcessBuilder. (map str cmd))
432+
(.directory working-dir))
433+
_ (.putAll (.environment proc-builder) (or env (:env ctx)))
434+
color (mod (hash (or prefix (first cmd))) 8)
435+
prefix (str "\u001b[" (+ 30 color) "m[" (or prefix (first cmd)) "]\u001b[0m ")
436+
process (pipe-process-output (.start proc-builder) prefix)
437+
ctx (update ctx :processes (fnil conj []) process)]
438+
(if background?
439+
ctx
440+
(let [exit (if timeout-ms
441+
(.waitFor process timeout-ms TimeUnit/MILLISECONDS)
442+
(.waitFor process))]
443+
(when (and check-exit-code? (not= 0 exit))
444+
(do
445+
(println (str prefix) "Exited with non-zero exit code: " exit)
446+
(System/exit exit)))
447+
ctx)))))
448+
449+
(defn start-clojure-process [{:keys [options aliases nrepl-port] :as ctx}]
450+
(let [args (clojure-cli-args ctx)]
451+
(apply debug (map shellquote args))
452+
((run-process {:cmd args
453+
:ctx-process-key :clojure-process
454+
:background? true}) ctx)))
455+
401456
(def before-steps [read-deps-edn
402457
handle-cli-args
403458
get-nrepl-port
@@ -414,41 +469,53 @@
414469
disable-stack-trace-elision
415470
inject-aliases-as-property
416471
include-watcher
417-
run-nrepl-server
418-
print-summary])
472+
print-summary
473+
run-nrepl-server])
419474

420475
(def after-steps [wait-for-nrepl
421476
;; stuff that happens after the server is up
422477
maybe-connect-emacs])
423478

424479
(def default-steps (concat before-steps
425-
[start-process]
480+
[start-clojure-process]
426481
after-steps))
427482

483+
(def ^:deprecated start-process start-clojure-process)
484+
428485
(defn find-project-root []
429486
(loop [dir (.getParent (io/file *file*))]
430487
(if (or (not dir)
431488
(.exists (io/file dir "deps.edn")))
432489
dir
433490
(recur (.getParent (io/file dir))))))
434491

435-
(defn initial-context [{:keys [steps executable project-root]
492+
(defn initial-context [{:keys [steps executable project-root
493+
middleware
494+
java-args
495+
eval-forms]
436496
:or {steps default-steps
437-
project-root (find-project-root)}}]
497+
project-root (find-project-root)
498+
middleware []
499+
java-args []
500+
eval-forms []}}]
438501
{:main-opts *command-line-args*
439502
:executable (or executable
440503
(str/replace *file*
441504
(str project-root "/")
442505
""))
443-
:project-root project-root})
506+
:project-root project-root
507+
:middleware middleware
508+
:java-args java-args
509+
:eval-forms eval-forms})
444510

445511
(defn process-steps [ctx steps]
446512
(reduce #(%2 %1) ctx steps))
447513

448514
(defn main
449515
([{:keys [steps] :or {steps default-steps} :as opts}]
450516
(let [ctx (process-steps (initial-context opts) steps)
451-
process (:clojure-process ctx)]
517+
processes (:processes ctx)]
452518
(.addShutdownHook (Runtime/getRuntime)
453-
(Thread. (fn [] (.destroy (:proc process)))))
454-
(System/exit (:exit @process)))))
519+
(Thread. (fn [] (run! #(.destroy %) processes))))
520+
(System/exit (apply min (for [p processes]
521+
(.waitFor p)))))))

0 commit comments

Comments
 (0)