(ns metabase.plugins (:require [clojure.core.memoize :as memoize] [clojure.java.classpath :as classpath] [clojure.java.io :as io] [clojure.string :as str] [environ.core :as env] [metabase.config :as config] [metabase.plugins.classloader :as classloader] [metabase.plugins.initialize :as plugins.init] [metabase.util.files :as u.files] [metabase.util.i18n :refer [trs]] [metabase.util.log :as log] [metabase.util.yaml :as yaml]) (:import (java.io File) (java.nio.file Files Path))) | |
(set! *warn-on-reflection* true) | |
(defn- plugins-dir-filename ^String [] (or (env/env :mb-plugins-dir) (.getAbsolutePath (io/file "plugins")))) | |
(def ^:private plugins-dir* ;; Memoized so we don't log the error messages multiple times if the plugins directory doesn't change (memoize/memo (fn [filename] (try ;; attempt to create <current-dir>/plugins if it doesn't already exist. Check that the directory is readable. (let [path (u.files/get-path filename)] (u.files/create-dir-if-not-exists! path) (assert (Files/isWritable path) (trs "Metabase does not have permissions to write to plugins directory {0}" filename)) {:path path, :temp false}) ;; If we couldn't create the directory, or the directory is not writable, fall back to a temporary directory ;; rather than failing to launch entirely. Log instructions for what should be done to fix the problem. (catch Throwable e (log/warn e (format "Metabase cannot use the plugins directory %s" filename) "\n" "Please make sure the directory exists and that Metabase has permission to write to it." "You can change the directory Metabase uses for modules by setting the environment variable MB_PLUGINS_DIR." "Falling back to a temporary directory for now.") ;; Check whether the fallback temporary directory is writable. If it's not, there's no way for us to ;; gracefully proceed here. Throw an Exception detailing the critical issues. (let [path (u.files/get-path (System/getProperty "java.io.tmpdir"))] (assert (Files/isWritable path) (trs "Metabase cannot write to temporary directory. Please set MB_PLUGINS_DIR to a writable directory and restart Metabase.")) {:path path, :temp true})))))) | |
Map with a :path key containing the | (defn plugins-dir-info ^Path [] (plugins-dir* (plugins-dir-filename))) |
Get a This is a wrapper around | (defn plugins-dir [] (:path (plugins-dir-info))) |
(defn- extract-system-modules! [] (when (io/resource "modules") (let [plugins-path (plugins-dir)] (u.files/with-open-path-to-resource [modules-path "modules"] (u.files/copy-files! modules-path plugins-path))))) | |
+----------------------------------------------------------------------------------------------------------------+ | loading/initializing plugins | +----------------------------------------------------------------------------------------------------------------+ | |
(defn- add-to-classpath! [^Path jar-path] (classloader/add-url-to-classpath! (-> jar-path .toUri .toURL))) | |
(defn- plugin-info [^Path jar-path] (some-> (u.files/slurp-file-from-archive jar-path "metabase-plugin.yaml") yaml/parse-string)) | |
Initiaize plugin using parsed info from a plugin maifest. Returns truthy if plugin was successfully initialized; falsey otherwise. | (defn- init-plugin-with-info! [info] (plugins.init/init-plugin-with-info! info)) |
Init plugin JAR file; returns truthy if plugin initialization was successful. | (defn- init-plugin! [^Path jar-path] (if-let [info (plugin-info jar-path)] ;; for plugins that include a metabase-plugin.yaml manifest run the normal init steps, don't add to classpath yet (init-plugin-with-info! (assoc info :add-to-classpath! #(add-to-classpath! jar-path))) ;; for all other JARs just add to classpath and call it a day (add-to-classpath! jar-path))) |
+----------------------------------------------------------------------------------------------------------------+ | load-plugins! | +----------------------------------------------------------------------------------------------------------------+ | |
(defn- plugins-paths [] (for [^Path path (u.files/files-seq (plugins-dir)) :when (and (u.files/regular-file? path) (u.files/readable? path) (str/ends-with? (.getFileName path) ".jar") (or (not (str/ends-with? (.getFileName path) "spark-deps.jar")) ;; if the JAR in question is the spark deps JAR we cannot load it because it's signed, and ;; the Metabase JAR itself as well as plugins no longer are; Java will throw an Exception ;; if different JARs with `metabase` packages have different signing keys. Go ahead and ;; ignore it but let people know they can get rid of it. (log/warn "spark-deps.jar is no longer needed by Metabase 0.32.0+. You can delete it from the plugins directory.")))] path)) | |
Return a sequence of [[java.io.File]] paths for | (when (or config/is-dev? config/is-test?) (defn- load-local-plugin-manifest! [^Path path] (some-> (slurp (str path)) yaml/parse-string plugins.init/init-plugin-with-info!)) (defn- driver-manifest-paths [] ;; only include plugin manifests if they're on the system classpath. (concat (for [^File file (classpath/system-classpath) :when (and (.isDirectory file) (not (.isHidden file)) (str/includes? (str file) "modules/drivers") (or (str/ends-with? (str file) "resources") (str/ends-with? (str file) "resources-ee"))) :let [manifest-file (io/file file "metabase-plugin.yaml")] :when (.exists manifest-file)] manifest-file) ;; for hacking on 3rd-party drivers locally: set ;; `-Dmb.dev.additional.driver.manifest.paths=/path/to/whatever/metabase-plugin.yaml` or ;; `MB_DEV_ADDITIONAL_DRIVER_MANIFEST_PATHS=...` to have that plugin manifest get loaded during startup. Specify ;; multiple plugin manifests by comma-separating them. (when-let [additional-paths (env/env :mb-dev-additional-driver-manifest-paths)] (map u.files/get-path (str/split additional-paths #","))))) (defn- load-local-plugin-manifests! [] ;; TODO - this should probably do an actual search in case we ever add any additional directories (doseq [manifest-path (driver-manifest-paths)] (log/infof "Loading local plugin manifest at %s" (str manifest-path)) (load-local-plugin-manifest! manifest-path)))) |
(defn- has-manifest? ^Boolean [^Path path] (boolean (u.files/file-exists-in-archive? path "metabase-plugin.yaml"))) | |
(defn- init-plugins! [paths] ;; sort paths so that ones that correspond to JARs with no plugin manifest (e.g. a dependency like the Oracle JDBC ;; driver `ojdbc8.jar`) always get initialized (i.e., added to the classpath) first; that way, Metabase drivers that ;; depend on them (such as Oracle) can be initialized the first time we see them. ;; ;; In Clojure world at least `false` < `true` so we can use `sort-by` to get non-Metabase-plugin JARs in front (doseq [^Path path (sort-by has-manifest? paths)] (try (init-plugin! path) (catch Throwable e (log/errorf e "Failied to initialize plugin %s" (.getFileName path)))))) | |
(defn- load! [] (log/infof "Loading plugins in %s..." (str (plugins-dir))) (extract-system-modules!) (let [paths (plugins-paths)] (init-plugins! paths)) (when (or config/is-dev? config/is-test?) (load-local-plugin-manifests!))) | |
(defonce ^:private loaded? (atom false)) | |
Load Metabase plugins. The are JARs shipped as part of Metabase itself, under the When loading plugins, Metabase performs the following steps:
This function will only perform loading steps the first time it is called — it is safe to call this function more than once. | (defn load-plugins! [] (when-not @loaded? (locking loaded? (when-not @loaded? (load!) (reset! loaded? true))))) |