Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
push:
branches:
- master
- feature/dockerfile
- fix/break-handling

jobs:
Config:
Expand Down Expand Up @@ -67,14 +67,20 @@ jobs:
bb: latest
- name: Update VERSION
run: echo "${{ needs.Config.outputs.version }}" > resources/VERSION
- name: Compile Java
run: bb java:compile
- name: Zip it
run: zip -r deps-try.zip . -x ".git/*"
- name: Build uberjar
run: |
rm -rf .git
# remove stuff not wanted in JAR
rm -rf .git .github
mv deps-try.zip ..
bb uberjar deps-try-bb.jar -m eval.deps-try

bb bb:uberjar
ls -lah deps-try-bb.jar
jar -tf deps-try-bb.jar

mv ../deps-try.zip .
- name: Testrun uberjar
run: bb deps-try-bb.jar -h
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
/.clj-kondo
.lsp
/VERSION
*.class
6 changes: 5 additions & 1 deletion bb.edn
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{:deps {io.github.eval/deps-try {:local/root "."}}
:bbin/bin {deps-try {:main-opts ["-m" "eval.deps-try"]}}
:tasks
{gen-manifest {:doc "Generate recipe-manifest and print to stdout"
{java:compile {:doc "Compile files under java/"
:task (shell {:dir "java"} "javac --release 11 dt/JvmtiAgent.java")}
bb:uberjar {:doc "Create the app's uberjar"
:task (shell "bb uberjar deps-try-bb.jar -m eval.deps-try")}
gen-manifest {:doc "Generate recipe-manifest and print to stdout"
:requires ([babashka.fs :as fs])
:task (exec 'eval.deps-try.recipe/generate&print-manifest
{:exec-args
Expand Down
6 changes: 3 additions & 3 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{:paths ["src" "resources" "."]
{:paths ["src" "resources" "." "java" "libdt/build"]
:deps {borkdude/edamame {:mvn/version "1.4.24"}
cider/orchard {:mvn/version "0.22.0"}
cider/orchard {:mvn/version "0.30.0"}
com.bhauman/rebel-readline {:local/root "./vendor/rebel-readline/rebel-readline"}
deps-try/cli {:local/root "./vendor/deps-try.cli"}
deps-try/http-client {:local/root "./vendor/deps-try.http-client"}
io.github.eval/compliment {:git/sha "cd9706db27d456e8940dcd1118174c94effa9358"}
org.clojure/clojure {:mvn/version "1.12.0-alpha11"}
org.clojure/clojure {:mvn/version "1.12.0"}
org.clojure/tools.gitlibs {:mvn/version "2.5.197"}
;; suppress logging
org.slf4j/slf4j-nop {:mvn/version "2.0.5"}}
Expand Down
24 changes: 24 additions & 0 deletions java/dt/JvmtiAgent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dt;

/**
* Java facade for the C side of the libnrepl JVMTI agent. Currently used to
* stop threads on JDK20+.
*/
public class JvmtiAgent {

// This will bind to Java_nrepl_JvmtiAgent_stopThread function once the
// agent containing this function will be attached.
public static native void stopThread(Thread thread, Throwable throwable);

/**
* Forcibly stop a given thread.
*/
public static void stopThread(Thread thread) {
// ThreadDeath is deprecated, but so it Thread.stop(). We can revisit
// this when JVM actually decides to remove this class.
@SuppressWarnings("deprecation")
Throwable throwable = new ThreadDeath();
throwable.setStackTrace(new StackTraceElement[0]);
stopThread(thread, throwable);
}
}
49 changes: 49 additions & 0 deletions libdt/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
LIB=libdt-$(OS)-$(ARCH).so
CXXFLAGS=-O3 -fno-exceptions -fvisibility=hidden
INCLUDES=-I$(JAVA_HOME)/include
LIBS=-ldl
SOURCES := $(wildcard src/*.c)

ifeq ($(JAVA_HOME),)
export JAVA_HOME:=$(shell java -cp . JavaHome)
endif

ARCH:=$(shell uname -m)
ifeq ($(ARCH),x86_64)
ARCH=x64
else
ifeq ($(findstring arm,$(ARCH)),arm)
ifeq ($(findstring 64,$(ARCH)),64)
ARCH=arm64
else
ARCH=arm32
endif
else ifeq ($(findstring aarch64,$(ARCH)),aarch64)
ARCH=arm64
else
ARCH=x86
endif
endif

OS:=$(shell uname -s)
ifeq ($(OS),Darwin)
CXXFLAGS += -arch x86_64 -arch arm64 -mmacos-version-min=10.12
INCLUDES += -I$(JAVA_HOME)/include/darwin
OS=macos
ARCH=universal
else
CXXFLAGS += -Wl,-z,defs
LIBS += -lrt
INCLUDES += -I$(JAVA_HOME)/include/linux
OS=linux
CC=gcc
endif

all: build/$(LIB)

clean:
$(RM) -r build

build/$(LIB): $(SOURCES)
mkdir -p build
$(CC) $(CXXFLAGS) $(INCLUDES) -fPIC -shared -o $@ $(SOURCES) $(LIBS)
Binary file added libdt/build/libdt-linux-arm64.so
Binary file not shown.
Binary file added libdt/build/libdt-linux-x64.so
Binary file not shown.
Binary file added libdt/build/libdt-macos-universal.so
Binary file not shown.
46 changes: 46 additions & 0 deletions libdt/src/dt_agent.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Native agent that nREPL uses for deeply buried JDK functionality.
// Currently it is used to bring back Thread.stop() that was lost in JDK20+.

#include <jvmti.h>
#include <stdio.h>

static jvmtiEnv* jvmti_env;

// https://docs.oracle.com/en/java/javase/21/docs/specs/jvmti.html#StopThread
JNIEXPORT void JNICALL Java_dt_JvmtiAgent_stopThread
(JNIEnv* env, jclass cls, jthread thread, jobject throwable) {
jvmtiError err;
jvmtiThreadInfo threadInfo;

err = (*jvmti_env)->GetThreadInfo(jvmti_env, thread, &threadInfo);
if (err != JVMTI_ERROR_NONE) {
printf("Error getting thread info: %d\n", err);
return;
}

//printf("Stopping thread \"%s\" using JVMTI...\n", threadInfo.name);

err = (*jvmti_env)->StopThread(jvmti_env, thread, throwable);
if (err != JVMTI_ERROR_NONE) {
printf("Error stopping thread: %d\n", err);
return;
}
}

JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* _) {
//printf("nREPL native agent loaded\n");

// Initialize JVMTI environment.
jint res = (*vm)->GetEnv(vm, (void**)&jvmti_env, JVMTI_VERSION_1_2);
if (res != JNI_OK) {
fprintf(stderr, "Failed to get JVMTI environment\n");
return JNI_ERR;
}

// Request capabilities for StopThread
jvmtiCapabilities capabilities = {0};
capabilities.can_signal_thread = 1;
(*jvmti_env)->AddCapabilities(jvmti_env, &capabilities);

return JNI_OK;
}
26 changes: 21 additions & 5 deletions src/eval/deps_try.clj
Original file line number Diff line number Diff line change
Expand Up @@ -100,19 +100,35 @@
;; TODO re-enable?
;; see https://github.com/clojure/brew-install/blob/1.11.3/CHANGELOG.md
#_(warn-unless-minimum-clojure-cli-version "1.11.1.1273" tdeps-version)
(let [paths (into ["."] ;; needed for clojure.java.io/resource
(string/split init-cp #":"))
(let [cp-parts (string/split init-cp #":")
;; When running from the packaged uberjar, its path is external to the
;; project and tools.deps deprecates such :paths entries, so it's added as
;; a :local/root dep instead. In dev the classpath is the project's own
;; source dirs and resolved deps, which belong in :paths as before.
;; "." is kept for clojure.java.io/resource.
{jars true, dirs false} (if dev?
{false cp-parts}
(group-by #(string/ends-with? % ".jar") cp-parts))
paths (into ["."] dirs)
self-deps (into {} (map-indexed (fn [i jar]
[(symbol "deps-try.self" (str "cp" i))
{:local/root jar}])
jars))
deps (merge
{'org.clojure/clojure {:mvn/version "1.12.0-alpha11"}}
{'org.clojure/clojure {:mvn/version "1.12.0"}}
self-deps
recipe-deps
requested-deps)
main-args (cond-> ["--version" version]
recipe-location (into (if ns-only
["--recipe-ns" recipe-location]
["--recipe" recipe-location]))
prepare (conj "-P"))]
(apply run-repl "-Sdeps" (str {:paths paths
:deps deps})
(apply run-repl
"-J-Djdk.attach.allowAttachSelf"
"-J-XX:+EnableDynamicAgentLoading"
"-Sdeps" (str {:paths paths
:deps deps})
"-M" "-m" "eval.deps-try.try" main-args)))

(defn- recipe-manifest-contents [{:keys [refresh] :as _cli-opts}]
Expand Down
3 changes: 3 additions & 0 deletions src/eval/deps_try/deps.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@
(defn deps-available []
(->> (clojure.java.basis/current-basis)
:libs
;; synthetic deps used to put the packaged uberjar on the classpath
;; (see eval.deps-try/start-repl!) shouldn't show as user libraries
(remove (comp #{"deps-try.self"} namespace key))
(filter (comp #(every? empty? %) :parents val)))))

#?(:bb nil
Expand Down
17 changes: 13 additions & 4 deletions src/eval/deps_try/try.clj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[eval.deps-try.history :as history]
[eval.deps-try.recipe :as recipe]
[eval.deps-try.rr-service :as rebel-service]
[eval.deps-try.util :as util]
[rebel-readline.clojure.line-reader :as clj-line-reader]
[rebel-readline.clojure.main :as rebel-main]
[rebel-readline.commands :as rebel-readline]
Expand Down Expand Up @@ -112,7 +113,11 @@
(defn- handle-sigint-form
[]
`(let [thread# (Thread/currentThread)]
(clj-repl/set-break-handler! (fn [_signal#] (.stop thread#)))))
(clj-repl/set-break-handler!
(fn [_signal#]
(if (<= util/java-version 19)
(.stop thread#)
((requiring-resolve 'eval.deps-try.util.jvmti/stop-thread) thread#))))))

(defn- recipe-instructions [{:keys [ns-only]}]
(str "Recipe" (when ns-only " namespace") " successfully loaded in the REPL-history. Press arrow-up to start with the first step."))
Expand Down Expand Up @@ -191,8 +196,12 @@
repl-opts (cond-> {:deps-try/version (:version opts)
:deps-try/data-path data-path
:caught (fn [ex]
(persist-just-caught ex)
(clojure.main/repl-caught ex))
(let [break-handled?
(= 'java.lang.ThreadDeath
(get-in (Throwable->map ex) [:via 1 :type]))]
(when-not break-handled?
(persist-just-caught ex)
(clojure.main/repl-caught ex))))
:init (fn []
(load-slow-deps!)
(apply require clojure.main/repl-requires)
Expand All @@ -210,6 +219,6 @@

(comment

(recipe/parse "/Users/gert/projects/deps-try/deps-try-recipes/recipes/next-jdbc/postgresql.clj")
(recipe/parse "/Users/gert/projects/deps-try/deps-try/recipes/next_jdbc/postgresql.clj")

#_:end)
13 changes: 13 additions & 0 deletions src/eval/deps_try/util.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@
[eval.deps-try.fs :as fs]
[eval.deps-try.process :as p]))

(defn parse-java-version
"Parse Java version string according to JEP 223 and return version as a number."
[]
(try (let [s (System/getProperty "java.specification.version")
[major minor _] (string/split s #"\.")
major (Integer/parseInt major)]
(if (> major 1)
major
(Integer/parseInt minor)))
(catch Exception _ 8)))

(def java-version "Current Java version number." (parse-java-version))

(defn duration->millis [{:keys [seconds minutes hours days weeks]
:or {seconds 0 minutes 0 hours 0 days 0 weeks 0}}]
(let [days (+ days (* weeks 7))
Expand Down
53 changes: 53 additions & 0 deletions src/eval/deps_try/util/jvmti.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
(ns eval.deps-try.util.jvmti
"This namespace contains code exclusive to JDK9+ and should not be attempted to
load with earlier JDKs."
(:require
[clojure.java.io :as io])
(:import
(com.sun.tools.attach VirtualMachine)
(java.lang ProcessHandle)
(java.nio.file Files)
(java.nio.file.attribute FileAttribute)
(dt JvmtiAgent)))

;;; Agent unpacking

(defonce ^:private temp-directory
(.toFile (Files/createTempDirectory "deps_try" (into-array FileAttribute []))))

(defn- unpack-from-jar [resource-name]
(let [path (io/file temp-directory resource-name)]
(if-let [resource (io/resource resource-name)]
(io/copy (io/input-stream resource) path)
(throw (ex-info (str "Could not find " resource-name " in resources.") {})))
(.getAbsolutePath path)))

(defn- macos? []
(re-find #"(?i)mac" (System/getProperty "os.name")))

(defn- aarch64? []
(re-find #"(?i)aarch64" (System/getProperty "os.arch")))

(def ^:private libdt-path
(delay
(let [lib (cond (macos?) "libdt-macos-universal.so"
(aarch64?) "libdt-linux-arm64.so"
:else "libdt-linux-x64.so")]
(unpack-from-jar lib))))

;;; Agent loading

(defn- attach-self ^VirtualMachine []
(VirtualMachine/attach (str (.pid (ProcessHandle/current)))))

(defn- load-libdt-agent []
(.loadAgentPath (attach-self) @libdt-path))

(def ^:private agent-loaded (delay (load-libdt-agent)))

(defn stop-thread
"Stop the given `thread` using JVMTI StopThread function. Risks state
corruption. Should not be used prior to JDK20."
[thread]
@agent-loaded
(JvmtiAgent/stopThread thread))
Loading