diff --git a/components/config/src/config/core.clj b/components/config/src/config/core.clj index a60545075..6d2e20ab6 100644 --- a/components/config/src/config/core.clj +++ b/components/config/src/config/core.clj @@ -1,6 +1,6 @@ (ns config.core - (:require [clojure.java.io :as io] - [clojure.edn :as edn])) + (:require [clojure.edn :as edn] + [clojure.java.io :as io])) ;;; Private vars @@ -33,6 +33,11 @@ (reset! config-file new-config-file) (reset! config-cache (read-config @config-file)))) +(defn merge-config! + "Deep-merges `overrides` into the cached config." + [overrides] + (swap! config-cache (fn [c] (merge-with merge (or c (read-config @config-file)) overrides)))) + (defn get-config "Retrieves the key `k` from the config file. Can also be called with the keys leading to a config. diff --git a/components/config/src/config/interface.clj b/components/config/src/config/interface.clj index a74341f0f..7f7ddb814 100644 --- a/components/config/src/config/interface.clj +++ b/components/config/src/config/interface.clj @@ -2,11 +2,15 @@ (:require [config.core :as core])) (def ^{:argslist '([] [config-file]) - :doc "Re/loads a configuration file. Defaults to the last loaded file, - or config.edn."} + :doc + "Re/loads a configuration file. Defaults to the last loaded file, or config.edn."} load-config core/load-config) -(def ^{:argslist '([& ks]) +(def ^{:argslist '([overrides]) + :doc "Deep-merges `overrides` into the cached config."} + merge-config! core/merge-config!) + +(def ^{:argslist '([& ks]) :doc "Retrieves the key `k` from the config file. Can also be called with the multiple keys leading to a config. @@ -21,6 +25,5 @@ ```clojure (get-config :mail) ; => {:host \"google.com\" :port 543} (get-config :mail :host) ; => \"google.com\" - ```" - } + ```"} get-config core/get-config) diff --git a/components/jcef/src/jcef/setup.clj b/components/jcef/src/jcef/setup.clj index e2be7f9ed..d4c47d85a 100644 --- a/components/jcef/src/jcef/setup.clj +++ b/components/jcef/src/jcef/setup.clj @@ -35,13 +35,18 @@ (defn jcef-builder "Produces a `CEFAppBuilder` with the installation - directory set according to the System OS." + directory set according to the System OS. + + Skips installation (i.e. no Chrome/CEF download) whenever an + install directory is already present on disk. In packaged + Conveyor mode (`app.dir` is set) installation is always skipped + because the bundle ships with the app." [] (let [app-dir (System/getProperty "app.dir") - jcef-dir (get-jcef-dir)] - (if (nil? app-dir) - (doto (CefAppBuilder.) - (.setInstallDir jcef-dir)) - (doto (CefAppBuilder.) - (.setInstallDir jcef-dir) - (.setSkipInstallation true))))) + jcef-dir (get-jcef-dir) + builder (doto (CefAppBuilder.) + (.setInstallDir jcef-dir))] + (when (or (some? app-dir) + (.exists jcef-dir)) + (.setSkipInstallation builder true)) + builder)) diff --git a/projects/behave/src/clj/behave/core.clj b/projects/behave/src/clj/behave/core.clj index ef78a0f3f..49270dbca 100644 --- a/projects/behave/src/clj/behave/core.clj +++ b/projects/behave/src/clj/behave/core.clj @@ -1,14 +1,12 @@ (ns behave.core (:gen-class) - (:import [javax.swing JFrame SwingUtilities UIManager] - [javax.imageio ImageIO]) - (:require [clojure.java.io :as io] - [behave.handlers :refer [create-cef-handler-stack]] - [behave.server :refer [init-config! init-db!]] - [file-utils.interface :refer [os-type app-data-dir]] + (:import [javax.imageio ImageIO] + [javax.swing JFrame SwingUtilities UIManager]) + (:require [behave.handlers :refer [create-cef-handler-stack]] + [behave.server :as server] + [clojure.java.io :as io] [config.interface :refer [get-config]] - [jcef.core :refer [show-dev-tools!]] - [jcef.interface :refer [create-cef-app! custom-request-handler show-loader!]] + [file-utils.interface :refer [os-type app-data-dir]] [logging.interface :as l :refer [log-str]])) ;;; Logging @@ -60,49 +58,81 @@ (defonce ^:private the-app (atom nil)) -(defn -main - "CEF client start method." - [& _args] - (init-config!) - (let [loader (show-loader! "Behave7" (io/resource "public/images/android-chrome-512x512.png")) - mode (get-config :server :mode) - http-port (or (get-config :server :http-port) 8080) - org-name (get-config :site :org-name) - app-name (get-config :site :app-name) - my-app-data-dir (app-data-dir org-name app-name) - log-config (if (= "prod" mode) - (assoc (get-config :logging) :log-dir (str (io/file my-app-data-dir "logs"))) - (get-config :logging)) - db-config (if (= "prod" mode) - (assoc-in (get-config :database :config) - [:store :path] - (str (io/file my-app-data-dir "db.sqlite"))) - (get-config :database :config)) - cache-path (str (io/file my-app-data-dir ".cache")) - request-handler (custom-request-handler - {:protocol "http" - :authority (format "localhost:%s" http-port) - :resource-dir "public" - :ring-handler (create-cef-handler-stack)})] +;;; Runtime Detection + +(defn- conveyor? + "True when running inside a Conveyor-packaged app (app.dir is set)." + [] + (some? (System/getProperty "app.dir"))) + +;;; Entry Points + +(defn- start-cef! + "Start the app in JCEF desktop mode." + [] + ;; Lazy-require jcef so server mode never loads jcef namespaces or + ;; org.cef.* classes. Only this code path pulls in the native bundle. + (let [show-loader! (requiring-resolve 'jcef.interface/show-loader!) + create-cef-app! (requiring-resolve 'jcef.interface/create-cef-app!) + custom-request-handler (requiring-resolve 'jcef.interface/custom-request-handler) + loader (show-loader! "Behave7" (io/resource "public/images/android-chrome-512x512.png")) + mode (get-config :server :mode) + http-port (or (get-config :server :http-port) 8080) + org-name (get-config :site :org-name) + app-name (get-config :site :app-name) + my-app-data-dir (app-data-dir org-name app-name) + log-config (if (= "prod" mode) + (assoc (get-config :logging) :log-dir (str (io/file my-app-data-dir "logs"))) + (get-config :logging)) + db-config (if (= "prod" mode) + (assoc-in (get-config :database :config) + [:store :path] + (str (io/file my-app-data-dir "db.sqlite"))) + (get-config :database :config)) + cache-path (str (io/file my-app-data-dir ".cache")) + request-handler (custom-request-handler + {:protocol "http" + :authority (format "localhost:%s" http-port) + :resource-dir "public" + :ring-handler (create-cef-handler-stack)})] (start-logging! log-config) - (init-db! db-config) + (server/init-db! db-config) (create-cef-app! - {:title (get-config :site :title) - :url (str "http://localhost:" http-port) - :cache-path cache-path - :fullscreen? true - :on-shown (fn [app & _] - (reset! the-app app) - (.dispose (:frame loader))) - :request-handler request-handler + {:title (get-config :site :title) + :url (str "http://localhost:" http-port) + :cache-path cache-path + :fullscreen? true + :on-shown (fn [app & _] + (reset! the-app app) + (.dispose (:frame loader))) + :request-handler request-handler :on-before-launch (fn [{:keys [frame]}] (on-before-launch frame (get-config :site :title)))}))) +(defn -main + "Unified entry point. Detects runtime environment and starts + in CEF desktop mode or HTTP server mode." + [& _args] + (if (conveyor?) + (do + (server/init-config!) + (server/enrich-config!) + (start-cef!)) + (do + (server/start-server!) + ;; `server.core/start-server!` runs Jetty with `:join? false`, and + ;; neither `vms-sync!` nor `watch-kill-signal!` blocks — so without + ;; parking here the main thread would return and the JVM would exit + ;; before any request could be served. Block until the process is + ;; killed (Ctrl-C / SIGTERM). + @(promise)))) + (comment (-main) + (start-cef!) ;; Dev Tools @the-app (require '[jcef.core :as jc]) diff --git a/projects/behave/src/clj/behave/handlers.clj b/projects/behave/src/clj/behave/handlers.clj index bf2d7baa1..4450c6680 100644 --- a/projects/behave/src/clj/behave/handlers.clj +++ b/projects/behave/src/clj/behave/handlers.clj @@ -1,7 +1,7 @@ (ns behave.handlers (:require [behave-routing.main :refer [routes]] [behave.download-vms :refer [export-from-vms export-images-from-vms]] - [behave.init :refer [init-handler]] + [behave.init :refer [active-clients init-handler]] [behave.open :refer [open-handler]] [behave.save :refer [save-handler]] [behave.sync :refer [sync-handler]] @@ -9,13 +9,12 @@ [bidi.bidi :refer [match-route]] [clojure.core.async :refer [ routing-handler (wrap-figwheel figwheel?) wrap-params wrap-keyword-params wrap-query-params wrap-req-content-type+accept - (wrap-resource "public" {:allow-symlinks? true}) - (wrap-content-type {:mime-types {"wasm" "application/wasm"}}) - wrap-multipart-params + (optional-middleware #(wrap-resource % "public" {:allow-symlinks? true}) (not cef?)) + (optional-middleware #(wrap-content-type % {:mime-types {"wasm" "application/wasm"}}) (not cef?)) + (optional-middleware wrap-multipart-params (not cef?)) wrap-exceptions (optional-middleware #(wrap-reload % {:dirs (reloadable-clj-files)}) reload?))) +(defn server-handler-stack + "Server handler stack." + [{:keys [reload? figwheel?]}] + (handler-stack {:reload? reload? :figwheel? figwheel?})) + (defn create-cef-handler-stack - "Custom handler stack for Chrome Embedded Framework." + "Handler stack for Chrome Embedded Framework." [] - (-> routing-handler - wrap-params - wrap-keyword-params - wrap-query-params - wrap-req-content-type+accept - wrap-exceptions)) + (handler-stack {:cef? true})) ;; This is for Figwheel (def ^{:doc "Figwheel handler."} diff --git a/projects/behave/src/clj/behave/init.clj b/projects/behave/src/clj/behave/init.clj index 59fa9b7cc..3d7df48da 100644 --- a/projects/behave/src/clj/behave/init.clj +++ b/projects/behave/src/clj/behave/init.clj @@ -10,6 +10,20 @@ [transport.interface :refer [clj-> mime->type]]) (:import (java.io ByteArrayInputStream))) +;;; Client Tracking + +(def active-clients + "Number of connected browser windows/tabs." + (atom 0)) + +(defn register-client! + "Increments the active client count. Call on each /api/init." + [] + (let [n (swap! active-clients inc)] + (log-str [:CLIENTS :register n]))) + +;;; Helpers + (defn- resource [s] (.getResource (ClassLoader/getSystemClassLoader) s)) @@ -25,7 +39,8 @@ (defn init-handler [{:keys [request-method accept] :as req}] (log-str "Request Received:" (select-keys req [:uri :request-method :params])) (let [res-type (or (mime->type accept) :edn)] - (when (and (= request-method :get)) + (when (= request-method :get) + (register-client!) (s/release-conn!) (reset! current-worksheet-atom nil) (init!) diff --git a/projects/behave/src/clj/behave/server.clj b/projects/behave/src/clj/behave/server.clj index 9ad3cdf33..dfabdd7da 100644 --- a/projects/behave/src/clj/behave/server.clj +++ b/projects/behave/src/clj/behave/server.clj @@ -1,10 +1,10 @@ (ns behave.server (:gen-class) - (:require [behave.store :as store] - [behave.handlers :refer [server-handler-stack vms-sync! watch-kill-signal!]] + (:require [behave.handlers :refer [server-handler-stack vms-sync! watch-kill-signal!]] + [behave.store :as store] [clojure.java.browse :refer [browse-url]] [clojure.java.io :as io] - [config.interface :refer [get-config load-config]] + [config.interface :refer [get-config load-config merge-config!]] [file-utils.interface :refer [os-path]] [logging.interface :as l :refer [log-str]] [server.interface :as server])) @@ -13,12 +13,23 @@ (defn init-config! [] (load-config (io/resource "config.edn"))) +(defn enrich-config! + "Computes runtime values and merges them into loaded config." + [] + (let [cef? (some? (System/getProperty "app.dir")) + mode (or (get-config :server :mode) + (if cef? "prod" "dev")) + jar-local? (and (= mode "prod") (not cef?))] + (merge-config! {:server {:mode mode} + :client {:jar-local? jar-local?}}))) + (defn init-db! "Initialize DB using configuration." [database-config] - (log-str [:DB-CONFIG database-config]) - (io/make-parents (get-in database-config [:store :path])) - (store/connect! database-config)) + (let [database-config (update-in database-config [:store :path] os-path)] + (log-str [:DB-CONFIG database-config]) + (io/make-parents (get-in database-config [:store :path])) + (store/connect! database-config))) ;;; Logging @@ -38,6 +49,7 @@ "Starts the Behave7 Application server." [] (init-config!) + (enrich-config!) (let [mode (get-config :server :mode) http-port (or (get-config :server :http-port) 8080) org-name (get-config :site :org-name)