Files
logseq/src/electron/electron/utils.cljs
Victor239 8b20877f7f fix(electron): repair custom URI scheme links
The npm `open` package (v8.4.2) is CommonJS with `module.exports = open;`
and no `.default` property, so importing it as `["open" :default open-external]`
left `open-external` undefined. Every custom-scheme click (freetube://, tg://,
etc.) reached `open-default-app!`, showed the confirmation dialog, and then
threw `TypeError: ...default is not a function` from inside (default-open url),
which Electron logged as an uncaughtException with no user-visible feedback.

Switch to `:as open-external` so the symbol binds to module.exports itself
(the open function). The neighboring node-fetch import is left as `:default`
because node-fetch v2.7.0 explicitly sets `exports.default = exports` for
ESM-interop, so it actually has a default export.

Regression from d6403b7746 (#12460), which replaced the prior working
`(defonce open (js/require "open"))` with the shadow-cljs ESM-style import.
2026-05-03 23:48:57 +08:00

298 lines
9.3 KiB
Clojure

(ns electron.utils
(:require ["electron" :refer [app BrowserWindow]]
["node-fetch" :default node-fetch]
["open" :as open-external]
["fs-extra" :as fs]
["path" :as node-path]
[cljs-bean.core :as bean]
[clojure.string :as string]
[electron.configs :as cfgs]
[electron.logger :as logger]
[logseq.cli.common.graph :as cli-common-graph]
[logseq.common.graph-dir :as graph-dir]
[logseq.common.config :as common-config]
[promesa.core :as p]
[shadow.esm :refer [dynamic-import]]))
(defonce *win (atom nil)) ;; The main window
(defonce mac? (= (.-platform js/process) "darwin"))
(defonce win32? (= (.-platform js/process) "win32"))
(defonce linux? (= (.-platform js/process) "linux"))
(defonce prod? (= js/process.env.NODE_ENV "production"))
(defonce dev? (not prod?))
(defonce *fetchAgent (atom nil))
(defonce *proxy-agent-ctors (atom nil))
(defonce extract-zip (js/require "extract-zip"))
(defn- <ensure-proxy-agent-ctors
"Load ESM-only proxy agent packages once and cache their constructors."
[]
(or @*proxy-agent-ctors
(reset! *proxy-agent-ctors
(p/let [https-module (dynamic-import "https-proxy-agent")
socks-module (dynamic-import "socks-proxy-agent")
ctors {:http (.-HttpsProxyAgent ^js https-module)
:socks5 (.-SocksProxyAgent ^js socks-module)}]
ctors))))
(defn open
([target] (open target nil))
([target options]
(if options
(open-external target (bean/->js options))
(open-external target))))
(defn fetch
([url] (fetch url nil))
([url options]
(node-fetch url (bean/->js (merge options {:agent @*fetchAgent})))))
(defn fix-win-path!
[path]
(when (not-empty path)
(if win32?
(string/replace path "\\" "/")
path)))
(defn to-native-win-path!
"Convert path to native win path"
[path]
(when (not-empty path)
(if win32?
(string/replace path "/" "\\")
path)))
(defn get-ls-dotdir-root
[]
(let [lg-dir (node-path/join (.getPath app "home") ".logseq")]
(when-not (fs/existsSync lg-dir)
(fs/mkdirSync lg-dir))
(fix-win-path! lg-dir)))
(defn get-ls-default-plugins
[]
(let [plugins-root (node-path/join (get-ls-dotdir-root) "plugins")
_ (when-not (fs/existsSync plugins-root)
(fs/mkdirSync plugins-root))
dirs (js->clj (fs/readdirSync plugins-root #js{"withFileTypes" true}))
dirs (->> dirs
(filter #(.isDirectory %))
(filter (fn [f] (not (some #(string/starts-with? (.-name f) %) ["_" "."]))))
(map #(node-path/join plugins-root (.-name %))))]
dirs))
(defn- set-fetch-agent-proxy
"Set proxy for fetch agent(plugin system)
protocol: http | socks5"
[{:keys [protocol host port]}]
(if (and protocol host port (or (= protocol "http") (= protocol "socks5")))
(p/let [ctors (<ensure-proxy-agent-ctors)
proxy-url (str protocol "://" host ":" port)]
(if-let [ctor (get ctors (keyword protocol))]
(reset! *fetchAgent (new ctor proxy-url))
(logger/error "Unknown proxy protocol:" protocol)))
(reset! *fetchAgent nil)))
(defn <set-electron-proxy
"Set proxy for electron
type: system | direct | socks5 | http"
([{:keys [type host port] :or {type "system"}}]
(let [->proxy-rules (fn [type host port]
(cond
(= type "http")
(str "http=" host ":" port ";https=" host ":" port)
(= type "socks5")
(str "http=socks5://" host ":" port ";https=socks5://" host ":" port)
(or (= type "socks") (= type "socks4"))
(str "http=socks://" host ":" port ";https=socks://" host ":" port)
(= type "direct")
"direct://"
:else
nil))
config (cond
(= type "system")
#js {:mode "system"}
(= type "direct")
#js {:mode "direct"}
(or (= type "socks5") (= type "http"))
#js {:mode "fixed_servers"
:proxyRules (->proxy-rules type host port)
:proxyBypassRules "<local>"}
:else
#js {:mode "system"})
sess (.. ^js @*win -webContents -session)]
(if sess
(p/do!
(.setProxy sess config)
(.forceReloadProxyConfig sess))
(p/resolved nil)))))
(defn- parse-pac-rule
"Parse Proxy Auto Config(PAC) line"
[line]
(let [parts (string/split line #"[ :]")
type (first parts)]
(cond
(= type "DIRECT")
nil
(and (contains? #{"PROXY" "HTTP" "SOCKS"} type)
(>= (count parts) 3))
{:protocol (if (= type "SOCKS") "socks5" "http")
:host (nth parts 1)
:port (nth parts 2)}
:else
(do
(logger/warn "Unknown PAC rule:" line)
nil))))
(defn <get-system-proxy
"Get system proxy for url, requires proxy to be set to system"
([] (<get-system-proxy "https://www.google.com"))
([for-url]
(when-let [sess (.. ^js @*win -webContents -session)]
(p/let [proxy (.resolveProxy sess for-url)
pac-opts (->> (string/split proxy #";")
(map parse-pac-rule)
(remove nil?))]
(when (seq pac-opts)
(first pac-opts))))))
(defn <set-proxy
"Set proxy for electron, fetch"
([{:keys [type host port] :or {type "system"} :as opts}]
(logger/info "set proxy to" opts)
(cond
(= type "system")
(p/let [_ (<set-electron-proxy {:type "system"})
proxy (<get-system-proxy)]
(set-fetch-agent-proxy proxy))
(= type "direct")
(p/let [_ (<set-electron-proxy {:type "direct"})]
(set-fetch-agent-proxy nil))
(or (= type "socks5") (= type "http"))
(p/let [_ (<set-electron-proxy {:type type :host host :port port})]
(set-fetch-agent-proxy {:protocol type :host host :port port}))
:else
(logger/error "Unknown proxy type:" type))))
(defn <restore-proxy-settings
"Restore proxy settings from configs.edn"
[]
(let [settings (cfgs/get-item :settings/agent)
settings (cond
(:type settings)
settings
;; migration from old config
(not-empty (:protocol settings))
(assoc settings :type (:protocol settings))
:else
{:type "system"})]
(logger/info "restore proxy settings" settings)
(<set-proxy settings)))
(defn save-proxy-settings
"Save proxy settings to configs.edn"
[{test' :test :keys [type host port] :or {type "system"}}]
(if (or (= type "system") (= type "direct"))
(cfgs/set-item! :settings/agent {:type type :test test'})
(cfgs/set-item! :settings/agent {:type type :protocol type :host host :port port :test test'})))
(defn read-file-raw
[path]
(fs/readFileSync path))
(defn read-file
[path]
(try
(when (fs/existsSync path)
(.toString (fs/readFileSync path)))
(catch :default e
(logger/error "Read file:" e))))
(defn get-focused-window
[]
(.getFocusedWindow BrowserWindow))
(defn get-win-from-sender
[^js evt]
(try
(.fromWebContents BrowserWindow (.-sender evt))
(catch :default _
nil)))
(defn send-to-renderer
"Notice: pass the `window` parameter if you can. Otherwise, the message
will not be received if there's no focused window.
Use `send-to-focused-renderer` instead if you want to set a window for fallback"
([kind payload]
(send-to-renderer (get-focused-window) kind payload))
([window kind payload]
(when window
(.. ^js window -webContents
(send (name kind) (bean/->js payload))))))
(defn send-to-focused-renderer
"Try to send to focused window. If no focused window, fallback to the `fallback-win`"
([kind payload fallback-win]
(let [focused-win (get-focused-window)
win (if focused-win focused-win fallback-win)]
(send-to-renderer win kind payload))))
(defn get-graph-dir
"required by all internal state in the electron section"
[graph-name]
(when (and (string? graph-name)
(string/starts-with? graph-name common-config/db-version-prefix))
(let [repo (common-config/canonicalize-db-version-repo graph-name)]
(node-path/join (cli-common-graph/get-db-graphs-dir)
(graph-dir/repo->encoded-graph-dir-name repo)))))
(comment
(defn get-graph-name
"Reverse `get-graph-dir`"
[graph-dir]
(str common-config/db-version-prefix (node-path/basename graph-dir))))
(defn decode-protected-assets-schema-path
[schema-path]
(cond-> schema-path
(string? schema-path)
(string/replace "/logseq__colon/" ":/")))
;; Keep update with the normalization in main
(defn normalize
[s]
(.normalize s "NFC"))
(defn normalize-lc
[s]
(normalize (string/lower-case s)))
(defn safe-decode-uri-component
[uri]
(try
(js/decodeURIComponent uri)
(catch :default _
(println "decodeURIComponent failed: " uri)
uri)))
(defn fs-stat->clj
[path]
(let [stat (fs/statSync path)]
{:size (.-size stat)
:mtime (.-mtime stat)
:ctime (.-ctime stat)}))