handler.cljs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. (ns electron.handler
  2. "This ns starts the event handling for the electron main process and defines
  3. all the application-specific event types"
  4. (:require ["electron" :refer [ipcMain dialog app autoUpdater shell]]
  5. [cljs-bean.core :as bean]
  6. ["fs" :as fs]
  7. ["buffer" :as buffer]
  8. ["fs-extra" :as fs-extra]
  9. ["path" :as path]
  10. ["os" :as os]
  11. ["diff-match-patch" :as google-diff]
  12. ["/electron/utils" :as js-utils]
  13. ["abort-controller" :as AbortController]
  14. [electron.fs-watcher :as watcher]
  15. [electron.configs :as cfgs]
  16. [promesa.core :as p]
  17. [clojure.string :as string]
  18. [electron.utils :as utils]
  19. [electron.state :as state]
  20. [clojure.core.async :as async]
  21. [electron.search :as search]
  22. [electron.git :as git]
  23. [electron.plugin :as plugin]
  24. [electron.window :as win]
  25. [electron.file-sync-rsapi :as rsapi]
  26. [electron.backup-file :as backup-file]
  27. [cljs.reader :as reader]
  28. [electron.find-in-page :as find]))
  29. (defmulti handle (fn [_window args] (keyword (first args))))
  30. (defmethod handle :mkdir [_window [_ dir]]
  31. (fs/mkdirSync dir))
  32. (defmethod handle :mkdir-recur [_window [_ dir]]
  33. (fs/mkdirSync dir #js {:recursive true}))
  34. ;; {encoding: 'utf8', withFileTypes: true}
  35. (defn- readdir
  36. [dir]
  37. (->> (tree-seq
  38. (fn [^js f]
  39. (.isDirectory (fs/statSync f) ()))
  40. (fn [d]
  41. (let [files (fs/readdirSync d (clj->js {:withFileTypes true}))]
  42. (->> files
  43. (remove #(.isSymbolicLink ^js %))
  44. (remove #(string/starts-with? (.-name ^js %) "."))
  45. (map #(.join path d (.-name %))))))
  46. dir)
  47. (doall)
  48. (vec)))
  49. (defmethod handle :readdir [_window [_ dir]]
  50. (readdir dir))
  51. (defmethod handle :unlink [_window [_ repo path]]
  52. (if (plugin/dotdir-file? path)
  53. (fs/unlinkSync path)
  54. (try
  55. (let [file-name (-> (string/replace path (str repo "/") "")
  56. (string/replace "/" "_")
  57. (string/replace "\\" "_"))
  58. recycle-dir (str repo "/logseq/.recycle")
  59. _ (fs-extra/ensureDirSync recycle-dir)
  60. new-path (str recycle-dir "/" file-name)]
  61. (fs/renameSync path new-path))
  62. (catch :default _e
  63. nil))))
  64. (defonce Diff (google-diff.))
  65. (defn string-some-deleted?
  66. [old new]
  67. (let [result (.diff_main Diff old new)]
  68. (some (fn [a] (= -1 (first a))) result)))
  69. (defmethod handle :backupDbFile [_window [_ repo path db-content new-content]]
  70. (when (and (string? db-content)
  71. (string? new-content)
  72. (string-some-deleted? db-content new-content))
  73. (backup-file/backup-file repo :backup-dir path (path/extname path) db-content)))
  74. (defmethod handle :addVersionFile [_window [_ repo path content]]
  75. (backup-file/backup-file repo :version-file-dir path (path/extname path) content))
  76. (defmethod handle :openFileBackupDir [_window [_ repo path]]
  77. (when (string? path)
  78. (let [dir (backup-file/get-backup-dir repo path)]
  79. (.openPath shell dir))))
  80. (defmethod handle :readFile [_window [_ path]]
  81. (utils/read-file path))
  82. (defn writable?
  83. [path]
  84. (assert (string? path))
  85. (try
  86. (fs/accessSync path (aget fs "W_OK"))
  87. (catch :default _e
  88. false)))
  89. (defmethod handle :writeFile [window [_ repo path content]]
  90. (let [^js Buf (.-Buffer buffer)
  91. ^js content (if (instance? js/ArrayBuffer content)
  92. (.from Buf content)
  93. content)]
  94. (try
  95. (when (and (fs/existsSync path) (not (writable? path)))
  96. (fs/chmodSync path "644"))
  97. (fs/writeFileSync path content)
  98. (fs/statSync path)
  99. (catch :default e
  100. (let [backup-path (try
  101. (backup-file/backup-file repo :backup-dir path (path/extname path) content)
  102. (catch :default e
  103. (.error utils/logger (str "Backup file failed: " e))))]
  104. (utils/send-to-renderer window "notification" {:type "error"
  105. :payload (str "Write to the file " path
  106. " failed, "
  107. e
  108. (when backup-path
  109. (str ". A backup file was saved to "
  110. backup-path
  111. ".")))}))))))
  112. (defmethod handle :rename [_window [_ old-path new-path]]
  113. (fs/renameSync old-path new-path))
  114. (defmethod handle :stat [_window [_ path]]
  115. (fs/statSync path))
  116. (defonce allowed-formats
  117. #{:org :markdown :md :edn :json :js :css :excalidraw})
  118. (defn get-ext
  119. [p]
  120. (-> (.extname path p)
  121. (subs 1)
  122. keyword))
  123. (defn- get-files
  124. [path]
  125. (let [result (->>
  126. (readdir path)
  127. (remove (partial utils/ignored-path? path))
  128. (filter #(contains? allowed-formats (get-ext %)))
  129. (map (fn [path]
  130. (let [stat (fs/statSync path)]
  131. (when-not (.isDirectory stat)
  132. {:path (utils/fix-win-path! path)
  133. :content (utils/read-file path)
  134. :stat stat}))))
  135. (remove nil?))]
  136. (vec (cons {:path (utils/fix-win-path! path)} result))))
  137. (defn open-dir-dialog []
  138. (p/let [result (.showOpenDialog dialog (bean/->js
  139. {:properties ["openDirectory" "createDirectory" "promptToCreate"]}))
  140. result (get (js->clj result) "filePaths")]
  141. (p/resolved (first result))))
  142. (defmethod handle :openDir [^js _window _messages]
  143. (p/let [path (open-dir-dialog)]
  144. (if path
  145. (p/resolved (bean/->js (get-files path)))
  146. (p/rejected (js/Error "path empty")))))
  147. (defmethod handle :getFiles [_window [_ path]]
  148. (get-files path))
  149. (defn- sanitize-graph-name
  150. [graph-name]
  151. (when graph-name
  152. (-> graph-name
  153. (string/replace "/" "++")
  154. (string/replace ":" "+3A+"))))
  155. (defn- graph-name->path
  156. [graph-name]
  157. (when graph-name
  158. (-> graph-name
  159. (string/replace "+3A+" ":")
  160. (string/replace "++" "/"))))
  161. (defn- get-graphs-dir
  162. []
  163. (let [dir (if utils/ci?
  164. (.resolve path js/__dirname "../tmp/graphs")
  165. (.join path (.homedir os) ".logseq" "graphs"))]
  166. (fs-extra/ensureDirSync dir)
  167. dir))
  168. (defn- get-graphs
  169. "Returns all graph names in the cache directory (strating with `logseq_local_`)"
  170. []
  171. (let [dir (get-graphs-dir)]
  172. (->> (readdir dir)
  173. (remove #{dir})
  174. (map #(path/basename % ".transit"))
  175. (map graph-name->path))))
  176. ;; TODO support alias mechanism
  177. (defn get-graph-name
  178. "Given a graph's name of string, returns the graph's fullname.
  179. E.g., given `cat`, returns `logseq_local_<path_to_directory>/cat`
  180. Returns `nil` if no such graph exists."
  181. [graph-identifier]
  182. (->> (get-graphs)
  183. (some #(when (string/ends-with? (utils/normalize-lc %)
  184. (str "/" (utils/normalize-lc graph-identifier)))
  185. %))))
  186. (defmethod handle :getGraphs [_window [_]]
  187. (get-graphs))
  188. (defn- read-txid-info!
  189. [root]
  190. (try
  191. (let [txid-path (.join path root "logseq/graphs-txid.edn")]
  192. (when (fs/existsSync txid-path)
  193. (when-let [sync-meta (and (not (string/blank? root))
  194. (.toString (.readFileSync fs txid-path)))]
  195. (reader/read-string sync-meta))))
  196. (catch js/Error _e
  197. (js/console.debug "[read txid meta] #" root (.-message _e)))))
  198. (defmethod handle :inflateGraphsInfo [_win [_ graphs]]
  199. (if (seq graphs)
  200. (for [{:keys [root] :as graph} graphs]
  201. (if-let [sync-meta (read-txid-info! root)]
  202. (assoc graph
  203. :sync-meta sync-meta
  204. :GraphUUID (second sync-meta))
  205. graph))
  206. []))
  207. (defmethod handle :readGraphTxIdInfo [_win [_ root]]
  208. (read-txid-info! root))
  209. (defn- get-graph-path
  210. [graph-name]
  211. (when graph-name
  212. (let [graph-name (sanitize-graph-name graph-name)
  213. dir (get-graphs-dir)]
  214. (.join path dir (str graph-name ".transit")))))
  215. (defn- get-serialized-graph
  216. [graph-name]
  217. (when graph-name
  218. (when-let [file-path (get-graph-path graph-name)]
  219. (when (fs/existsSync file-path)
  220. (utils/read-file file-path)))))
  221. (defmethod handle :getSerializedGraph [_window [_ graph-name]]
  222. (get-serialized-graph graph-name))
  223. (defmethod handle :saveGraph [_window [_ graph-name value-str]]
  224. ;; NOTE: graph-name is a plain "local" for demo graph.
  225. (when (and graph-name value-str (not (= "local" graph-name)))
  226. (when-let [file-path (get-graph-path graph-name)]
  227. (fs/writeFileSync file-path value-str))))
  228. (defmethod handle :deleteGraph [_window [_ graph-name]]
  229. (when graph-name
  230. (when-let [file-path (get-graph-path graph-name)]
  231. (when (fs/existsSync file-path)
  232. (fs-extra/removeSync file-path)))))
  233. (defmethod handle :persistent-dbs-saved [_window _]
  234. (async/put! state/persistent-dbs-chan true)
  235. true)
  236. (defmethod handle :search-blocks [_window [_ repo q opts]]
  237. (search/search-blocks repo q opts))
  238. (defmethod handle :rebuild-blocks-indice [_window [_ repo data]]
  239. (search/truncate-blocks-table! repo)
  240. ;; unneeded serialization
  241. (search/upsert-blocks! repo (bean/->js data))
  242. (search/write-search-version! repo)
  243. [])
  244. (defmethod handle :transact-blocks [_window [_ repo data]]
  245. (let [{:keys [blocks-to-remove-set blocks-to-add]} data]
  246. (when (seq blocks-to-remove-set)
  247. (search/delete-blocks! repo blocks-to-remove-set))
  248. (when (seq blocks-to-add)
  249. ;; unneeded serialization
  250. (search/upsert-blocks! repo (bean/->js blocks-to-add)))))
  251. (defmethod handle :truncate-blocks [_window [_ repo]]
  252. (search/truncate-blocks-table! repo))
  253. (defmethod handle :remove-db [_window [_ repo]]
  254. (search/delete-db! repo))
  255. (defn clear-cache!
  256. [window]
  257. (let [graphs-dir (get-graphs-dir)]
  258. (fs-extra/removeSync graphs-dir))
  259. (let [path (.getPath ^object app "userData")]
  260. (doseq [dir ["search" "IndexedDB"]]
  261. (let [path (path/join path dir)]
  262. (try
  263. (fs-extra/removeSync path)
  264. (catch js/Error e
  265. (.error utils/logger (str "Clear cache: " e))))))
  266. (utils/send-to-renderer window "redirect" {:payload {:to :home}})))
  267. (defmethod handle :clearCache [window _]
  268. (search/close!)
  269. (clear-cache! window)
  270. (search/ensure-search-dir!))
  271. (defmethod handle :openDialog [^js _window _messages]
  272. (open-dir-dialog))
  273. (defmethod handle :copyDirectory [^js _window [_ src dest opts]]
  274. (fs-extra/copy src dest opts))
  275. (defmethod handle :getLogseqDotDirRoot []
  276. (utils/get-ls-dotdir-root))
  277. (defmethod handle :testProxyUrl [win [_ url]]
  278. (p/let [_ (utils/fetch url)]
  279. (utils/send-to-renderer win :notification {:type "success" :payload (str "Successfully: " url)})))
  280. (defmethod handle :httpFetchJSON [_win [_ url options]]
  281. (p/let [res (utils/fetch url options)
  282. json (.json res)]
  283. json))
  284. (defmethod handle :getUserDefaultPlugins []
  285. (utils/get-ls-default-plugins))
  286. (defmethod handle :validateUserExternalPlugins [_win [_ urls]]
  287. (zipmap urls (for [url urls]
  288. (try
  289. (and (fs-extra/pathExistsSync url)
  290. (fs-extra/pathExistsSync (path/join url "package.json")))
  291. (catch js/Error _e false)))))
  292. (defmethod handle :relaunchApp []
  293. (.relaunch app) (.quit app))
  294. (defmethod handle :quitApp []
  295. (.quit app))
  296. (defmethod handle :userAppCfgs [_window [_ k v]]
  297. (let [config (cfgs/get-config)]
  298. (if-not k
  299. config
  300. (if-not (nil? v)
  301. (cfgs/set-item! (keyword k) v)
  302. (cfgs/get-item (keyword k))))))
  303. (defmethod handle :getDirname [_]
  304. js/__dirname)
  305. (defmethod handle :getAppBaseInfo [^js win [_ _opts]]
  306. {:isFullScreen (.isFullScreen win)})
  307. (defmethod handle :getAssetsFiles [^js win [_ {:keys [exts]}]]
  308. (when-let [graph-path (state/get-window-graph-path win)]
  309. (p/let [^js files (js-utils/getAllFiles (.join path graph-path "assets") (clj->js exts))]
  310. files)))
  311. (defn close-watcher-when-orphaned!
  312. "When it's the last window for the directory, close the watcher."
  313. [window graph-path]
  314. (when (not (win/graph-has-other-windows? window graph-path))
  315. (watcher/close-watcher! graph-path)))
  316. (defn set-current-graph!
  317. [window graph-path]
  318. (let [old-path (state/get-window-graph-path window)]
  319. (when (and old-path graph-path (not= old-path graph-path))
  320. (close-watcher-when-orphaned! window old-path))
  321. (swap! state/state assoc :graph/current graph-path)
  322. (swap! state/state assoc-in [:window/graph window] graph-path)
  323. nil))
  324. (defmethod handle :setCurrentGraph [^js window [_ graph-name]]
  325. (when graph-name
  326. (set-current-graph! window (utils/get-graph-dir graph-name))))
  327. (defmethod handle :runGit [_ [_ args]]
  328. (when (seq args)
  329. (git/raw! args)))
  330. (defmethod handle :runGitWithinCurrentGraph [_ [_ args]]
  331. (when (seq args)
  332. (git/init!)
  333. (git/run-git2! (clj->js args))))
  334. (defmethod handle :gitCommitAll [_ [_ message]]
  335. (git/add-all-and-commit! message))
  336. (defmethod handle :installMarketPlugin [_ [_ mft]]
  337. (plugin/install-or-update! mft))
  338. (defmethod handle :updateMarketPlugin [_ [_ pkg]]
  339. (plugin/install-or-update! pkg))
  340. (defmethod handle :uninstallMarketPlugin [_ [_ id]]
  341. (plugin/uninstall! id))
  342. (def *request-abort-signals (atom {}))
  343. (defmethod handle :httpRequest [_ [_ _req-id opts]]
  344. (let [{:keys [url abortable method data returnType headers]} opts]
  345. (when-let [[method type] (and (not (string/blank? url))
  346. [(keyword (string/upper-case (or method "GET")))
  347. (keyword (string/lower-case (or returnType "json")))])]
  348. (-> (utils/fetch url
  349. (-> {:method method
  350. :headers (and headers (bean/->js headers))}
  351. (merge (when (and (not (contains? #{:GET :HEAD} method)) data)
  352. ;; TODO: support type of arrayBuffer
  353. {:body (js/JSON.stringify (bean/->js data))})
  354. (when-let [^js controller (and abortable (AbortController.))]
  355. (swap! *request-abort-signals assoc _req-id controller)
  356. {:signal (.-signal controller)}))))
  357. (p/then (fn [^js res]
  358. (case type
  359. :json
  360. (.json res)
  361. :arraybuffer
  362. (.arrayBuffer res)
  363. :base64
  364. (-> (.buffer res)
  365. (p/then #(.toString % "base64")))
  366. :text
  367. (.text res))))
  368. (p/catch
  369. (fn [^js e]
  370. ;; TODO: handle special cases
  371. (throw e)))
  372. (p/finally
  373. (fn []
  374. (swap! *request-abort-signals dissoc _req-id)))))))
  375. (defmethod handle :httpRequestAbort [_ [_ _req-id]]
  376. (when-let [^js controller (get @*request-abort-signals _req-id)]
  377. (.abort controller)))
  378. (defmethod handle :quitAndInstall []
  379. (.quitAndInstall autoUpdater))
  380. (defmethod handle :graphUnlinked [^js _win [_ repo]]
  381. (doseq [window (win/get-all-windows)]
  382. (utils/send-to-renderer window "graphUnlinked" (bean/->clj repo))))
  383. (defmethod handle :dbsync [^js _win [_ graph tx-data]]
  384. (let [dir (utils/get-graph-dir graph)]
  385. (doseq [window (win/get-graph-all-windows dir)]
  386. (utils/send-to-renderer window "dbsync"
  387. (bean/->clj {:graph graph
  388. :tx-data tx-data})))))
  389. (defmethod handle :graphHasOtherWindow [^js win [_ graph]]
  390. (let [dir (utils/get-graph-dir graph)]
  391. (win/graph-has-other-windows? win dir)))
  392. (defmethod handle :graphHasMultipleWindows [^js _win [_ graph]]
  393. (let [dir (utils/get-graph-dir graph)
  394. windows (win/get-graph-all-windows dir)]
  395. (> (count windows) 1)))
  396. (defmethod handle :addDirWatcher [^js _window [_ dir options]]
  397. ;; receive dir path (not repo / graph) from frontend
  398. ;; Windows on same dir share the same watcher
  399. ;; Only close file watcher when:
  400. ;; 1. there is no one window on the same dir
  401. ;; 2. reset file watcher to resend `add` event on window refreshing
  402. (when dir
  403. (watcher/watch-dir! dir options)))
  404. (defmethod handle :unwatchDir [^js _window [_ dir]]
  405. (when dir
  406. (watcher/close-watcher! dir)))
  407. (defn open-new-window!
  408. "Persist db first before calling! Or may break db persistency"
  409. []
  410. (let [win (win/create-main-window)]
  411. (win/on-close-actions! win close-watcher-when-orphaned!)
  412. (win/setup-window-listeners! win)
  413. win))
  414. (defmethod handle :openNewWindow [_window [_]]
  415. (open-new-window!)
  416. nil)
  417. (defmethod handle :graphReady [window [_ graph-name]]
  418. (when-let [f (:window/once-graph-ready @state/state)]
  419. (f window graph-name)
  420. (state/set-state! :window/once-graph-ready nil)))
  421. (defmethod handle :searchVersionChanged?
  422. [^js _win [_ graph]]
  423. (search/version-changed? graph))
  424. (defmethod handle :reloadWindowPage [^js win]
  425. (when-let [web-content (.-webContents win)]
  426. (.reload web-content)))
  427. (defmethod handle :setHttpsAgent [^js _win [_ opts]]
  428. (utils/set-fetch-agent opts))
  429. ;;;;;;;;;;;;;;;;;;;;;;;
  430. ;; file-sync-rs-apis ;;
  431. ;;;;;;;;;;;;;;;;;;;;;;;
  432. (defmethod handle :key-gen [_]
  433. (rsapi/key-gen))
  434. (defmethod handle :set-env [_ args]
  435. (apply rsapi/set-env (rest args)))
  436. (defmethod handle :get-local-files-meta [_ args]
  437. (apply rsapi/get-local-files-meta (rest args)))
  438. (defmethod handle :get-local-all-files-meta [_ args]
  439. (apply rsapi/get-local-all-files-meta (rest args)))
  440. (defmethod handle :rename-local-file [_ args]
  441. (apply rsapi/rename-local-file (rest args)))
  442. (defmethod handle :delete-local-files [_ args]
  443. (apply rsapi/delete-local-files (rest args)))
  444. (defmethod handle :update-local-files [_ args]
  445. (apply rsapi/update-local-files (rest args)))
  446. (defmethod handle :download-version-files [_ args]
  447. (apply rsapi/download-version-files (rest args)))
  448. (defmethod handle :delete-remote-files [_ args]
  449. (apply rsapi/delete-remote-files (rest args)))
  450. (defmethod handle :update-remote-file [_ args]
  451. (apply rsapi/update-remote-file (rest args)))
  452. (defmethod handle :update-remote-files [_ args]
  453. (apply rsapi/update-remote-files (rest args)))
  454. (defmethod handle :decrypt-fnames [_ args]
  455. (apply rsapi/decrypt-fnames (rest args)))
  456. (defmethod handle :encrypt-fnames [_ args]
  457. (apply rsapi/encrypt-fnames (rest args)))
  458. (defmethod handle :encrypt-with-passphrase [_ args]
  459. (apply rsapi/encrypt-with-passphrase (rest args)))
  460. (defmethod handle :decrypt-with-passphrase [_ args]
  461. (apply rsapi/decrypt-with-passphrase (rest args)))
  462. (defmethod handle :default [args]
  463. (.error utils/logger "Error: no ipc handler for: " (bean/->js args)))
  464. (defn broadcast-persist-graph!
  465. "Receive graph-name (not graph path)
  466. Sends persist graph event to the renderer contains the target graph.
  467. Returns a promise<void>."
  468. [graph-name]
  469. (p/create (fn [resolve _reject]
  470. (let [graph-path (utils/get-graph-dir graph-name)
  471. windows (win/get-graph-all-windows graph-path)
  472. tar-graph-win (first windows)]
  473. (if tar-graph-win
  474. ;; if no such graph, skip directly
  475. (do (state/set-state! :window/once-persist-done #(resolve nil))
  476. (utils/send-to-renderer tar-graph-win "persistGraph" graph-name))
  477. (resolve nil))))))
  478. (defmethod handle :broadcastPersistGraph [^js _win [_ graph-name]]
  479. (broadcast-persist-graph! graph-name))
  480. (defmethod handle :broadcastPersistGraphDone [^js _win [_]]
  481. ;; main process -> renderer doesn't support promise, so we use a global var to store the callback
  482. (when-let [f (:window/once-persist-done @state/state)]
  483. (f)
  484. (state/set-state! :window/once-persist-done nil)))
  485. (defmethod handle :find-in-page [^js win [_ search option]]
  486. (find/find! win search (bean/->js option)))
  487. (defmethod handle :clear-find-in-page [^js win [_]]
  488. (find/clear! win))
  489. (defn set-ipc-handler! [window]
  490. (let [main-channel "main"]
  491. (.handle ipcMain main-channel
  492. (fn [^js event args-js]
  493. (try
  494. (let [message (bean/->clj args-js)]
  495. (bean/->js (handle (or (utils/get-win-from-sender event) window) message)))
  496. (catch js/Error e
  497. (when-not (contains? #{"mkdir" "stat"} (nth args-js 0))
  498. (.error utils/logger "IPC error: " (str {:event event
  499. :args args-js})
  500. e))
  501. e))))
  502. #(.removeHandler ipcMain main-channel)))