shadow-cljs compile命令提示构建完成但进程不退出的问题
Shadow-cljs compile/release 进程不退出问题排查与解决
问题背景
使用Clojure/ClojureScript、shadow-cljs、re-frame等技术栈开发动态Web应用,本地shadow-cljs watch app运行正常,但执行shadow-cljs compile app或release app时,命令输出Build completed后进程始终不退出,表现与watch命令一致。该问题在本地和GitHub Actions环境均复现,导致CI环境持续计费。已排除user.clj、默认Build hooks、预启动shadow-cljs start进程等影响,怀疑与项目中的自定义宏有关。
原因分析
从提供的宏代码来看,问题大概率出在编译期宏执行引发的资源残留或非守护线程未终止:
- 自定义
slurp宏在编译期执行文件存在性检查与读取操作,虽然clojure.core/slurp会自动关闭流,但(.exists (io/file file))的文件系统操作可能触发某些类加载,意外启动非守护线程; read-open-config宏中调用System/getenv获取环境变量,编译期执行系统环境调用可能导致JVM加载额外的系统级资源,这些资源未在编译完成后被正确清理;- 宏的编译期执行逻辑存在潜在竞态(比如文件检查后读取前文件被删除),可能引发未捕获的资源泄漏,导致JVM无法正常退出。
解决方案
方案1:优化宏的实现逻辑
修改自定义宏,避免不必要的文件检查,改用异常捕获处理文件不存在的情况,同时确保编译期操作的简洁性:
(defmacro slurp [file & [default]] (try (clojure.core/slurp file) (catch java.io.FileNotFoundException _ default))) (defmacro read-open-config [env-var] (let [config-path (str "config/" (System/getenv env-var) "-open.edn")] (try (clojure.edn/read-string (slurp config-path)) (catch java.io.FileNotFoundException _ {}))))
方案2:替换宏为编译前Hook注入(推荐)
避免在宏中执行编译期IO操作,改用shadow-cljs的Build Hooks在编译前读取配置,通过closure-defines注入到ClojureScript代码中,彻底消除宏带来的潜在问题:
- 在
shadow-cljs.edn中添加Build Hook配置:
:build-hooks [my.build/config-hook]
- 创建
src/my/build.clj实现Hook逻辑:
(ns my.build (:require [clojure.java.io :as io] [clojure.edn :as edn] [shadow.cljs.build :as build])) (defn config-hook [build-state] (let [env-name (or (System/getenv "ENV_VAR") "dev") config-file (io/file "config" (str env-name "-open.edn")) config (if (.exists config-file) (edn/read-string (slurp config-file)) {})] ;; 将配置注入到ClojureScript的全局常量中 (assoc-in build-state [:compiler-options :closure-defines 'my.config/open-config] config)))
- 在ClojureScript代码中引用配置:
(ns my.config) (def ^:const open-config js/my.config.open-config)
方案3:排查残留线程(用于定位根因)
本地执行shadow-cljs compile app后,用以下命令排查残留的非守护线程:
- 用
jps找到shadow-cljs进程ID; - 执行
jstack <进程ID>查看线程栈,找到未终止的非守护线程,定位其来源(比如第三方依赖或内部代码启动的线程),针对性修复。
内容的提问来源于stack exchange,提问作者Pedro Delfino
相关产品推荐
相关产品推荐

