2

I am writing a CLI framework in Clojure called OneCLI. The main center piece of this framework is a function called go! which parses the command line, environment variables, and config files "for you" and runs one of several different user provided functions based on what was provided in those inputs.

Typically, go! is called from the -main function of the user's calling Clojure program. I use my own library, for example, in another "uberjar" style app called zic. The function go! calls System/exit as part of its run, passing it an exit code that comes from the result of the user provided function. This works great "in production", but it also means that I can't run the zic.cli/-main function from the REPL, as whenever I do it calls System/exit and the REPL exits.

Before you ask, running it from the REPL while developing on a raspberry pi avoids the expensive 45 seconds it takes to run lein uberjar/1 minute 30 seconds to run clj -X:depstar uberjar :jar ....

My question is: Is there some var or value I can check as part of Clojure's standard library that tells my OneCLI code whether it's running from the REPL or if it's running from a JAR?

Such a variable would enable me in OneCLI to detect that we're running from a REPL so that it can avoid calling System/exit.

djhaskin987
  • 9,741
  • 4
  • 50
  • 86
  • 1
    For those future people that just need a quick and dirty way of telling if the *current code* (won't work for my problem above) is in a REPL: just check the `*file*` var, it's unset (or set to some default) when working from inside a REPL. – djhaskin987 Jan 11 '21 at 16:25
  • similar question here https://stackoverflow.com/questions/30409104/how-to-check-whether-clojure-code-is-being-evaluated-inside-a-repl – Andy Feb 28 '21 at 09:02
  • this doesn't work for me - `*file*` is set to /tmp/some-long-filename in my REPL (linux with lein repl 2.9.5, clojure 1.10.1) – Andy Feb 28 '21 at 10:32
  • Does this answer your question? [How to check whether Clojure code is being evaluated inside a REPL?](https://stackoverflow.com/questions/30409104/how-to-check-whether-clojure-code-is-being-evaluated-inside-a-repl) – skylize Sep 02 '22 at 19:16

4 Answers4

6

Instead of trying to have one function that magically detects what environment you're running from, it's quite simple to just have two functions that behave differently.

  • Extract out the shared behavior to a function that is not part of -main. Call it run or whatever.
  • Have -main call that function, and then call System/exit
  • When you wish to use the program from a repl, call run instead of -main. It will finish normally, and not call System/exit.
amalloy
  • 89,153
  • 8
  • 140
  • 205
  • 1
    I agree this is a good way to go, but the `go!` function is in the OneCLI library and the `-main` function is declared in the zic project. This solution involves changing the interface between the library and the application. At the moment OneCLI doesn't have much adoption, so that's probably fine, but it is a more substantial change. – mange Jan 11 '21 at 09:41
  • 1
    Decoupling running and error handling would also allow for easier testing. – cfrick Jan 11 '21 at 11:26
  • 1
    The good news is, since `System/exit` is the most destructive thing on the planet, it's easy to make that interface change compatibly. Change the application first: anything it does after `go!` returns is harmless, because `go!` deletes the universe. Then you can change `go!` to not call `System/exit` anymore, and the dead code that was in the application becomes your new cleanup code. – amalloy Jan 11 '21 at 11:41
  • 1
    @mange I can simply make a new function in the OneCLI library with the modified behavior, then call that function from `go!`, so I don't actually have the library's interface, I could just add more functionality while still following the advice. – djhaskin987 Jan 11 '21 at 16:12
3

I don't know how to detect if you're running at a REPL. I took a quick look through Clojure's launching code (clojure.main), but I didn't see any hooks to detect whether you're in a REPL compared to something run via clojure -m.

If you're using AOT (like you are in zic) then you could check whether any of the "REPL" variables (*1, *2, *3, and *e) are bound.

;; returns true in a REPL and `clojure -m`, and
;; returns false in an AOT jar file run with java -jar
(bound? #'*1) 

This solves your question as it was asked, but I don't love this "magical" mechanism of guessing the programmer's intent. It might work for your use case (given I think AOT saves on startup time, and CLI tools probably want to start quickly), but none of the projects I work on use AOT at all.

Another option to solve your problem in the clojure -m case as well would be to require developers to explicitly opt out of the "exit on completion" behaviour. One way to do that could be to use a property.

(defn maybe-exit [exit-code]
  (cond
    (= (System/getProperty "onecli.oncompletion") "remain") (System/exit exit-code)
    (= exit-code 0) nil
    :else (throw (ex-info "Command completed unsuccessfully" {:exit-code exit-code}))))

Using this code, in a development environment you can add

:jvm-opts ["-Donecli.oncompletion=remain"]

to your deps.edn or project.clj file, but leave it out when running "in production". This has the advantage of being more explicit, but the cost is that developers have to be more explicit.

mange
  • 3,172
  • 18
  • 27
  • 1
    The idea of using properties is very interesting. As an aside, I did find out that the `*file*` variable in `clojure.core` is unset for code *currently inside a REPL*, for code that's trying to find out if it istelf is in a REPL (though this wouldn't help for libraries called from a REPL but shipped in JARs, as is in my case). – djhaskin987 Jan 11 '21 at 16:27
  • 1
    It looks like `*file*` is bound when I use `lein repl`, when I use `cider-jack-in`, and when I use `clojure`. How did you get it to be unbound? – mange Jan 11 '21 at 20:16
  • 1
    I think it was bound, but not set to an actual file name, more like "DEFAULT_PATH" or something. – djhaskin987 Jan 13 '21 at 19:25
2

This is an interesting question because it's usually dreadful to put JVM shutdown into a library, but on the other hand a "real app" involves lots of boilerplate that would be great to share... such as hiding the jar's splash gif at the right time, or (re)opening a Windows terminal if the app wants stdio.

Your uberjar will contain clojure.main, so it is quite possible (and useful) to run the REPL in your uberjar (java -cp my-whole-app.jar clojure.main). Therefore, "detecting" clues on the classpath might not help.

Instead, manage JVM-shutdown work in the -main in the namespace that your jar's manifest declares as its Main-Class. That is: if you run it as java -jar my-whole-app.jar, then it should shut everything down properly.

But I do not always want -main to shut everything down, you say. Then you need two -mains. Make a second -main in a different namespace. Let the jar's Main-Class -main do absolutely nothing but (1) delegate to the second main and (2) shut down the JVM at the end. When you're in the REPL, invoke the second -main, the one that won't clobber the JVM. You can factor out most of each -main into a library. If you went "full framework" you could even make the framework own the uberjarring process and the Main-Class.

Biped Phill
  • 1,181
  • 8
  • 13
1

Every Java JAR file must have the file META-INF/MANIFEST.MF added. If it isn't present, you cannot be running in a (normal) JAR file. While you could fool this detector by putting a bogus file on the classpath (i.e. in ./resources, for example), it is a reliable way of detecting a normal JAR file.


Problem:

Dependency JAR files are sometimes sloppy and will pollute the classpath with their own META-INF/MANIFEST.MF files, so the presence of any random META-INF/MANIFEST.MF is not enough to determine the answer in the presence of "noise" files. So, you need to check for the existence of your own specific META-INF/MANIFEST.MF file. This is easy to do if you know the Maven values for ArtifactId and GroupId.

In a Leiningen project, the first line of project.clj looks like

(defproject demo-grp/demo-art "0.1.0-SNAPSHOT"

for a group ID of demo-grp and an artifact ID of demo-art. If your file looks like this:

(defproject demo "0.1.0-SNAPSHOT"

then both the group ID and artifact ID will be demo. Your particular MANIFEST.MF will look like

> cat META-INF/MANIFEST.MF 
Manifest-Version: 1.0
Created-By: Leiningen 2.9.1
Built-By: alan
Build-Jdk: 15
Leiningen-Project-ArtifactId: demo-art
Leiningen-Project-GroupId: demo-grp
Leiningen-Project-Version: 0.1.0-SNAPSHOT
Main-Class: demo.core

Set up a function using the to ID strings to detect the presence of your particular project MANIFEST.MF:

(ns demo.core
  (:require [clojure.java.io :as io])
  (:gen-class))

(def ArtifactId "demo-art")
(def GroupId "demo-grp")

(defn jar-file? []
  (let [re-ArtifactId (re-pattern (str ".*ArtifactId.*" ArtifactId))
        re-GroupId    (re-pattern (str ".*GroupId.*" GroupId))
        manifest      (slurp (io/resource "META-INF/MANIFEST.MF"))
        f1            (re-find re-ArtifactId manifest)
        f2            (re-find re-GroupId manifest)
        found?        (boolean (and f1 f2))]
    found?))

(defn -main []
  (println "main - enter")
  (println "Detected JAR file: " (jar-file?))
  )

You can now test the code:

~/expr/demo > lein clean ; lein run
main - enter
Detected JAR file:  false

~/expr/demo > lein clean ; lein uberjar
Compiling demo.core
Created /home/alan/expr/demo/target/uberjar/demo-art-0.1.0-SNAPSHOT.jar
Created /home/alan/expr/demo/target/uberjar/demo-art-0.1.0-SNAPSHOT-standalone.jar

~/expr/demo > java -jar /home/alan/expr/demo/target/uberjar/demo-art-0.1.0-SNAPSHOT-standalone.jar 
main - enter
Detected JAR file:  true

Example of "noise" JAR file: If we do a lein clean; lein run, and add a line to our main program

(println (slurp (io/resource "META-INF/MANIFEST.MF")))

we get out:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: jenkins
Created-By: Apache Maven 3.2.5
Build-Jdk: 1.8.0_111

I have no idea where this is coming from to get on the CLASSPATH.


P.S. for Leiningen JAR files

When using lein to build a JAR file, it always places a copy of the project.clj file at the location:

META-INF/leiningen/demo-grp/demo-art/project.clj

so you could also use this file's presence/absence as a detector.


Update

OK, it looks like the the MANIFEST.MF file is highly dependent on your build tool. See

So, your choices appear to be:

  1. For lein, you can use the above technique.
  2. You could use the REPL trick of *1 from the other answer.
  3. You could always have your build tool include a custom key-value pair in the manifest and then detect that.

Update #2

An alternate answer, and perhaps easier, is to use the lein-environ plugin and environ library (you need both) to detect the environment (assuming you are using lein to create your REPL). Your project.clj should look like:

  :dependencies [
                 [clojure.java-time "0.3.2"]
                 [environ "1.2.0"]
                 [org.clojure/clojure "1.10.2-alpha1"]
                 [prismatic/schema "1.1.12"]
                 [tupelo "21.01.05"]
                 ]
  :plugins [[com.jakemccrary/lein-test-refresh "0.24.1"]
            [lein-ancient "0.6.15"]
            [lein-codox "0.10.7"]
            [lein-environ "1.2.0"]
            ]

and you need a profiles.clj:

{:dev  {:env {:env-mode "dev"}}
 :test {:env {:env-mode "test"}}
 :prod {:env {:env-mode "prod"}}}

and a namespace demo.config like:

(ns demo.config
  (:require
    [environ.core :as environ]
  ))

(def ^:dynamic *env-mode* (environ/env :env-mode))
(println "  *env-mode* => " *env-mode*)

And then you get results like:

*env-mode* =>  dev      ; for `lein run`
*env-mode* =>  test     ; for `lein test`
*env-mode* =>  nil      ; from `java -jar ...`

You need to type:

lein with-profile :prod run

to produce

*env-mode* =>  prod
Alan Thompson
  • 29,276
  • 6
  • 41
  • 48
  • Seems a bit hacky. – djhaskin987 Jan 11 '21 at 02:31
  • 1
    It's very hacky and it won't work with a lot of the JAR bundlers -- it just happens to work with Leiningen, but it's relying on quirks of Leiningen that are undocumented. – Sean Corfield Jan 11 '21 at 04:32
  • 1
    @AlanThompson I found also another way: create a record with no fields e.g. `(defrecord Reference [])` and use the java reflection way to detect if we're in a JAR file or not: https://mkyong.com/java/java-get-the-name-or-path-of-a-running-jar-file/ you can call the non-static way in that link on the record class itself. – djhaskin987 Jan 11 '21 at 16:19
  • I like the custom key-value pair in the manifest idea as a more general solution to JAR detection. – djhaskin987 Jan 11 '21 at 16:23
  • Yes. That is exactly what `Leiningen-Project-ArtifactId` is: a custom `lein` key/value pair (although it is related to the Maven coordinates). The `lein` JAR file also has the custom `META-INF/leiningen/demo-grp/demo-art/project.clj` file. – Alan Thompson Jan 11 '21 at 18:44
  • @djhaskin987 the DefRecord approach seems pretty hacky too but it works for me and doesn't require any build-time configuration, so that's the solution I went with – Andy Feb 28 '21 at 10:26