# Motrix Admin & Developer Guide

This guide is the complete operational and architectural reference for **Motrix** — a cross-platform desktop download manager built on Electron, Vue, and the aria2 engine. It is written for administrators, packagers, security reviewers, and developers who need to understand how Motrix is built, configured, secured, deployed, and extended.

Everything here is derived directly from the Motrix source tree at commit `7012040` (version `1.8.19`, dated 2023-06-07). Where a behavior carries a security or operational caveat, it is documented honestly rather than glossed over.

> [!NOTE]
> For end-user instructions (installing, adding downloads, managing tasks), see the companion **User Guide** at [User Guide](/guides/agalwood/Motrix/user-guide/).

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Architecture](#2-architecture)
3. [System Requirements](#3-system-requirements)
4. [Configuration Mastery](#4-configuration-mastery)
5. [The aria2 Engine Option Reference](#5-the-aria2-engine-option-reference)
6. [Security Model](#6-security-model)
7. [Operations](#7-operations)
8. [Build & Distribution](#8-build--distribution)
9. [Module Reference (Internals)](#9-module-reference-internals)
10. [Code Quality & Patterns](#10-code-quality--patterns)
11. [Troubleshooting (Operational)](#11-troubleshooting-operational)
12. [Potential Improvements](#12-potential-improvements)
13. [Quick Reference](#13-quick-reference)

---

## 1. Executive Summary

Motrix is an **Electron desktop application** that wraps the [aria2](https://aria2.github.io/) command-line download engine in a Vue-based UI. It is a single-user, offline-first desktop tool — there is no server component, no multi-user mode, and no cloud backend.

| Attribute | Value |
|-----------|-------|
| Product | Motrix — "A full-featured download manager" |
| Version (this doc) | `1.8.19` |
| License | MIT (© 2018–present Dr_rOot) |
| App ID (bundle) | `app.motrix.native` |
| Platforms | Windows (x64/ia32), macOS (x64/arm64/universal), Linux (x64/arm64/armv7l) |
| Desktop framework | Electron `^22.3.9` |
| UI framework | Vue `2.7.14` + Vuex `3.6.2` + vue-router `3.6.5` + Element UI `2.15.13` |
| Download engine | Bundled aria2 (`aria2c`), driven over JSON-RPC |
| i18n | 25 locales via `@panter/vue-i18next` / `i18next` |
| Build system | Webpack 5 + `electron-builder` 24 (electron-vue project layout) |
| Node (build) | `>= 16.0.0` |

**Architectural one-liner:** A privileged Electron **main process** spawns and supervises a bundled `aria2c` child process, then both the main process and the Vue **renderer** drive that engine over a local JSON-RPC websocket. Persistent state lives in two `electron-store` JSON files (system + user) plus aria2's own session file.

> [!IMPORTANT]
> Motrix uses Electron's **older, less-sandboxed** security model (`@electron/remote` enabled, no context isolation, direct `ipcRenderer` use). Combined with the bundled aria2's RPC defaults (`rpc-listen-all=true`, empty secret), this has real security implications on shared networks. See [§6 Security Model](#6-security-model) — this is the most important section for reviewers.

---

## 2. Architecture

### 2.1 Process model

Motrix follows the classic Electron two-process split, plus a **third, external** process: the aria2 engine.

```
                          ┌─────────────────────────────────────────────┐
                          │              Motrix (Electron)               │
                          │                                              │
   OS events              │   ┌──────────────────┐   IPC: 'command' /   │
  (open-url,    ───────────►  │   MAIN PROCESS    │   'event' channels   │
   open-file,   argv      │   │   (Node.js)       │◄────────┐            │
   protocol)              │   │                   │         │            │
                          │   │  Launcher         │   ┌─────┴─────────┐  │
                          │   │  Application      │   │  RENDERER     │  │
                          │   │  ├ ConfigManager  │   │  (Chromium)   │  │
                          │   │  ├ Engine ────────┼───┤  Vue 2 +      │  │
                          │   │  ├ EngineClient   │   │  Vuex +       │  │
                          │   │  ├ WindowManager  │   │  Element UI   │  │
                          │   │  ├ Tray/Menu/Dock │   │               │  │
                          │   │  ├ UPnPManager    │   │  api/Api.js ──┼──┼──┐
                          │   │  ├ ProtocolManager│   │  store/*      │  │  │
                          │   │  └ UpdateManager  │   └───────────────┘  │  │
                          │   └─────────┬─────────┘                      │  │
                          └─────────────┼────────────────────────────────┘  │
                                        │ spawn() child process              │
                                        ▼                                    │
                          ┌─────────────────────────┐   JSON-RPC over        │
                          │   aria2c (bundled)       │◄──ws 127.0.0.1:16800───┘
                          │   --conf-path=aria2.conf │   (both processes
                          │   --save-session=…       │    connect to the
                          └─────────────────────────┘    same RPC endpoint)
```

Three independent connections to the engine exist:
1. **Main process → engine** (`EngineClient`) — used for global option changes, shutdown, and resume-on-launch.
2. **Renderer → engine** (`api/Api.js`) — the primary path: add/pause/resume/remove tasks, poll status, fetch peers.
3. The engine reads its **session file** to restore tasks across restarts.

### 2.2 Component inventory

| Component | File | Responsibility |
|-----------|------|----------------|
| **Launcher** | `src/main/Launcher.js` | Single-instance lock, app lifecycle (`ready`/`activate`/`will-quit`), URL/file/argv intake, `@electron/remote` init |
| **Application** | `src/main/Application.js` | The orchestrator: instantiates all managers, wires IPC, watches config changes, drives the engine |
| **ConfigManager** | `src/main/core/ConfigManager.js` | Two `electron-store` stores (system + user) with defaults, fix-up, get/set/reset |
| **Engine** | `src/main/core/Engine.js` | Spawns/stops the `aria2c` child process; builds its CLI args from config |
| **EngineClient** | `src/main/core/EngineClient.js` | Main-process JSON-RPC client (`call`, `changeGlobalOption`, `shutdown`) |
| **WindowManager** | `src/main/ui/WindowManager.js` | Creates/shows/hides BrowserWindows, persists window state |
| **MenuManager / TrayManager / DockManager / TouchBarManager / ThemeManager** | `src/main/ui/*` | Native UI surfaces (app menu, tray, dock, Touch Bar, theme) |
| **UPnPManager** | `src/main/core/UPnPManager.js` | UPnP/NAT-PMP port mapping via `@motrix/nat-api` |
| **ProtocolManager** | `src/main/core/ProtocolManager.js` | Registers `mo`/`motrix`/`magnet`/`thunder` schemes; routes incoming URLs |
| **UpdateManager** | `src/main/core/UpdateManager.js` | `electron-updater` integration, proxy-aware, manual download/install |
| **AutoLaunchManager** | `src/main/core/AutoLaunchManager.js` | Open-at-login via `app.setLoginItemSettings` |
| **EnergyManager** | `src/main/core/EnergyManager.js` | `powerSaveBlocker` while downloading |
| **ExceptionHandler** | `src/main/core/ExceptionHandler.js` | Global `uncaughtException` handler + error dialog |
| **Logger** | `src/main/core/Logger.js` | `electron-log` wrapper, portable-aware path |
| **Renderer API** | `src/renderer/api/Api.js` | Renderer-side aria2 RPC client + config bridge |
| **Vuex store** | `src/renderer/store/modules/*` | `app`, `task`, `preference` state modules |
| **Aria2 RPC lib** | `src/shared/aria2/lib/*` | Minimal JSON-RPC client (`Aria2`, `JSONRPCClient`, `Deferred`) |
| **Shared constants/config keys** | `src/shared/constants.js`, `configKeys.js` | Enums, ports, key lists |

### 2.3 Startup sequence

From `Application.init()`, in order: Context → ConfigManager → Logger → LocaleManager → application menu → WindowManager → UPnPManager → **start engine** → EngineClient → ThemeManager → TrayManager → TouchBarManager → DockManager → AutoLaunchManager → EnergyManager → ProtocolManager → UpdaterManager → command handlers → event handlers → IPC message/invoke handlers → emit `application:initialized`.

On `initialized`, Motrix runs **auto-sync trackers**, **auto-resume tasks** (if enabled), and **adjusts the menu** for the Mac App Store build.

### 2.4 The IPC surface (the "API")

There is no HTTP API. The renderer↔main contract is three IPC channels, registered in `Application.handleIpcMessages()` / `handleIpcInvokes()`:

| Channel | Direction | Payload | Purpose |
|---------|-----------|---------|---------|
| `command` | renderer → main | `(command, ...args)` | Fire an application command; re-emitted on the `Application` EventEmitter |
| `event` | renderer → main | `(eventName, ...args)` | Fire an internal event |
| `get-app-config` | renderer → main (invoke) | — → merged config object | Returns `{ ...systemConfig, ...userConfig, ...context }` |

Commands are plain string event names (e.g. `application:save-preference`, `application:new-task`, `application:quit`, `application:reveal-in-folder`, `application:factory-reset`). The full set is registered in `Application.handleCommands()`.

> [!WARNING]
> Because the renderer can emit **any** command string over the `command` channel and the main process re-emits it on its EventEmitter, the IPC boundary is effectively unrestricted. This is the basis of GitHub issue **#1796** ("Unrestricted IPC command emission allows arbitrary `shell.openExternal` and internal command invocation"). See [§6.3](#63-ipc-trust-boundary).

---

## 3. System Requirements

### 3.1 Runtime (end-user) requirements

| Platform | Minimum |
|----------|---------|
| Windows | Windows 7+ (Scoop build notes Win7+); x64 or ia32 |
| macOS | A macOS version supported by Electron 22 |
| Linux | glibc-based distro; x64, arm64, or armv7l. AppImage needs FUSE / AppImageLauncher |

Disk: enough for the app (~150–200 MB installed) plus your downloads. Network: open BT/DHT listen ports for optimal BitTorrent performance (UPnP can handle this automatically).

### 3.2 Build requirements

| Tool | Version |
|------|---------|
| Node.js | `>= 16.0.0` |
| Package manager | Yarn (project is yarn-locked) |
| Electron | `^22.3.9` |
| electron-builder | `^24.4.0` |
| Python / build tools | As required by native deps (`@motrix/nat-api`, etc.) |

> [!NOTE]
> The project is structured as an **electron-vue** app: build orchestration lives in `.electron-vue/` (build.js, dev-runner.js, webpack configs), invoked by the `package.json` scripts. The `postinstall` step runs `electron-builder install-app-deps` to rebuild native modules against Electron's ABI.

---

## 4. Configuration Mastery

Motrix keeps **two** configuration stores, both as `electron-store` JSON files in the per-user data directory. Understanding the split is essential.

### 4.1 The system / user split

| Store | File | Holds | Consumed by |
|-------|------|-------|-------------|
| **System config** | `system.json` | aria2 engine options (downloads, BT, ports, RPC, proxy) | Passed to aria2 as CLI args / `changeGlobalOption` |
| **User config** | `user.json` | App preferences (theme, locale, run mode, startup, UPnP) | The Electron app itself |

`ConfigManager` (`src/main/core/ConfigManager.js`) owns both. The key lists live in `src/shared/configKeys.js`:
- `userKeys` — 28 app-preference keys.
- `systemKeys` — ~110 aria2 option keys (the allow-list of options Motrix will pass to the engine).
- `needRestartKeys` — keys that require an engine/app restart: `dht-listen-port`, `hide-app-menu`, `listen-port`, `rpc-listen-port`, `rpc-secret`.

> [!IMPORTANT]
> **aria2 configuration priority:** `system.json` overrides the built-in `aria2.conf`, which is documented in the file header (`system.json > built-in aria2.conf`). `ConfigManager.fixSystemConfig()` deletes any stored key that isn't a recognized aria2 option (`separateConfig().others`), so the engine never receives garbage flags.

### 4.2 User config reference (defaults)

From `ConfigManager.initUserConfig()`:

| Key | Default | Meaning |
|-----|---------|---------|
| `auto-check-update` | `is.macOS()` | Auto-check for updates (on by default only on macOS) |
| `auto-hide-window` | `false` | Hide window on blur |
| `auto-sync-tracker` | `true` | Daily tracker-list sync |
| `enable-upnp` | `true` | UPnP/NAT-PMP port mapping |
| `engine-max-connection-per-server` | `64` | UI cap for per-task splits |
| `favorite-directories` | `[]` | Pinned save folders (max 5) |
| `hide-app-menu` | `is.windows() || is.linux()` | Hide native menu bar on Win/Linux |
| `history-directories` | `[]` | Recent save folders (max 5) |
| `keep-seeding` | `false` | Seed indefinitely |
| `keep-window-state` | `false` | Persist window bounds |
| `last-check-update-time` | `0` | Bookkeeping |
| `last-sync-tracker-time` | `0` | Bookkeeping |
| `locale` | `app.getLocale()` | UI language |
| `log-level` | `'warn'` | electron-log file level |
| `new-task-show-downloading` | `true` | Jump to Downloading after add |
| `no-confirm-before-delete-task` | `false` | Skip delete confirmation |
| `open-at-login` | `false` | Launch at login |
| `protocols` | `{ magnet: true, thunder: false }` | Default-handler registration |
| `proxy` | `{ enable:false, server:'', bypass:'', scope:[download,update-app,update-trackers] }` | App/engine proxy |
| `resume-all-when-app-launched` | `false` | Auto-resume on launch |
| `run-mode` | `1` (Standard) | Standard / Tray / Hide-Tray |
| `show-progress-bar` | `true` | Taskbar/dock progress bar |
| `task-notification` | `true` | Desktop notifications |
| `theme` | `'auto'` | auto / light / dark |
| `tracker-source` | ngosang best + best_ip (CDN) | Tracker list sources |
| `tray-theme` | `'auto'` | Tray icon theme |
| `tray-speedometer` | `is.macOS()` | Menu-bar speed meter (macOS) |
| `update-channel` | `'latest'` | electron-updater channel |
| `window-state` | `{}` | Persisted window bounds per page |

### 4.3 System config defaults (subset shipped in `system.json`)

`ConfigManager.initSystemConfig()` seeds these defaults (the rest fall through to the built-in `aria2.conf`):

| Key | Default |
|-----|---------|
| `dht-listen-port` | `26701` |
| `listen-port` | `21301` |
| `rpc-listen-port` | `16800` (`ENGINE_RPC_PORT`) |
| `rpc-secret` | `''` (empty) |
| `max-concurrent-downloads` | `5` |
| `max-connection-per-server` | `64` |
| `split` | `64` |
| `max-download-limit` / `max-overall-download-limit` / `max-overall-upload-limit` | `0` (unlimited) |
| `seed-ratio` | `2` |
| `seed-time` | `2880` (minutes) |
| `continue` | `true` |
| `auto-file-renaming` | `true` |
| `follow-metalink` / `follow-torrent` | `true` |
| `bt-load-saved-metadata` / `bt-save-metadata` | `true` |
| `enable-dht6` | `true` |
| `pause` | `true` |
| `user-agent` | Chrome UA string |
| `all-proxy` / `no-proxy` / `bt-tracker` / `bt-exclude-tracker` | `''` |
| `dht-file-path` / `dht-file-path6` | `dht.dat` / `dht6.dat` in user data |

> [!NOTE]
> Mock User-Agent presets are defined in `src/shared/ua.js`: `aria2/1.36.0`, `Transmission/3.00`, a Chrome 111 UA (the default), and a Baidu NetDisk UA. The Chrome UA is used by default so generic servers don't reject the engine.

### 4.4 Proxy configuration

Proxy is a structured user-config object with **scopes** (`src/shared/constants.js` → `PROXY_SCOPES`):

| Scope | Applies to |
|-------|-----------|
| `download` | aria2 downloads (sets `all-proxy` / `no-proxy` on the engine) |
| `update-app` | `electron-updater` traffic (via `updater.netSession.setProxy`) |
| `update-trackers` | Tracker-list fetches |

When `proxy.enable && server && scope.includes('download')`, `Application.watchProxyChange()` pushes `{ 'all-proxy': server, 'no-proxy': bypass }` into the system config and calls `changeGlobalOption` live. The updater parses credentials from a `user:pass@host` URL and registers a `login` handler for authenticated proxies.

---

## 5. The aria2 Engine Option Reference

Motrix passes engine options to aria2 two ways: at spawn time (`Engine.getStartArgs()` → `transformConfig()` renders each non-empty key as `--key=value`) and live (`changeGlobalOption`). The complete allow-list of options Motrix recognizes is `systemKeys` in `src/shared/configKeys.js`. Any of these may be set in `system.json`.

### 5.1 Built-in `aria2.conf` baseline

Each platform ships an `aria2.conf` under `extra/{platform}/{arch}/engine/`. The baseline (Windows x64) sets:

| Group | Settings |
|-------|----------|
| **RPC** | `enable-rpc=true`, `rpc-allow-origin-all=true`, `rpc-listen-all=true` |
| **Filesystem** | `auto-save-interval=10`, `disk-cache=64M`, `file-allocation=falloc`, `no-file-allocation-limit=64M`, `save-session-interval=10` |
| **Task** | `bt-detach-seed-only=true`, `check-certificate=false`, `max-file-not-found=10`, `max-tries=0`, `retry-wait=10`, `connect-timeout=10`, `timeout=10`, `min-split-size=1M`, `http-accept-gzip=true`, `remote-time=true`, `summary-interval=0`, `content-disposition-default-utf8=true` |
| **BitTorrent** | `bt-enable-lpd=true`, `bt-hash-check-seed=true`, `bt-max-peers=128`, `bt-prioritize-piece=head`, `bt-remove-unselected-file=true`, `bt-seed-unverified=false`, `bt-tracker-connect-timeout=10`, `bt-tracker-timeout=10`, `dht-entry-point=dht.transmissionbt.com:6881`, `enable-dht=true`, `enable-dht6=true`, `enable-peer-exchange=true`, `peer-agent=Transmission/3.00`, `peer-id-prefix=-TR3000-` |

> [!CAUTION]
> `check-certificate=false` disables TLS certificate verification for the engine. This is a deliberate convenience trade-off (many download mirrors have broken certs) but means HTTPS downloads are not protected against MITM at the transport layer. Reviewers in high-security environments should be aware of this and can override it via `system.json` (`check-integrity`/`checksum` are available for content verification).

### 5.2 Notable system option groups (from `systemKeys`)

The ~110 recognized keys group into:

- **Connections & speed:** `max-concurrent-downloads`, `max-connection-per-server`, `split`, `min-split-size`, `max-download-limit`, `max-overall-download-limit`, `max-overall-upload-limit`, `max-upload-limit`, `lowest-speed-limit`.
- **Retry & timeout:** `max-tries`, `retry-wait`, `connect-timeout`, `timeout`, `max-file-not-found`, `max-resume-failure-tries`.
- **HTTP/FTP:** `header`, `referer`, `user-agent`, `http-user`/`http-passwd`, `http-proxy*`, `https-proxy*`, `ftp-user`/`ftp-passwd`, `ftp-pasv`, `ftp-type`, `http-accept-gzip`, `http-no-cache`, `conditional-get`, `content-disposition-default-utf8`.
- **BitTorrent:** `bt-tracker`, `bt-exclude-tracker`, `bt-max-peers`, `bt-request-peer-speed-limit`, `bt-min-crypto-level`, `bt-require-crypto`, `bt-force-encryption`, `bt-seed-unverified`, `bt-save-metadata`, `bt-load-saved-metadata`, `bt-stop-timeout`, `bt-tracker-interval`, `enable-peer-exchange`, `bt-enable-lpd`, `seed-ratio`, `seed-time`.
- **DHT / ports:** `listen-port`, `dht-listen-port`, `dht-file-path`, `dht-file-path6`.
- **RPC:** `rpc-listen-port`, `rpc-secret`, `rpc-save-upload-metadata`.
- **Files:** `dir`, `out`, `allow-overwrite`, `auto-file-renaming`, `file-allocation`, `no-file-allocation-limit`, `enable-mmap`, `force-save`, `remove-control-file`, `remote-time`, `index-out`, `select-file`.
- **Metalink:** `follow-metalink`, `metalink-*`.

For the authoritative semantics of each option, see the [aria2 manual](https://aria2.github.io/manual/en/html/aria2c.html) — Motrix passes them through verbatim.

### 5.3 How seeding options are applied

`Engine.getStartArgs()` contains special logic: if the user enabled **keep-seeding** *or* `seed-ratio === 0`, Motrix forces `seed-ratio=0` and **deletes** `seed-time` from the args — telling aria2 to seed without a ratio cap or time limit.

---

## 6. Security Model

This is the most important section for security reviewers. Motrix predates the modern Electron hardening playbook, and its bundled engine has permissive RPC defaults. None of this is a "bug" in the sense of being accidental — but it defines a real trust boundary.

### 6.1 Electron process configuration

- `@electron/remote` is **enabled** for every window (`Launcher.handleRendererRemote()` calls `require('@electron/remote/main').enable(window.webContents)`), and `initialize()` runs in `src/main/index.js`.
- The renderer uses Node/Electron APIs directly (`ipcRenderer`, `electron-is`), indicating **`nodeIntegration: true`** and **no `contextIsolation`** (the hardened `contextBridge` pattern is not used).
- `process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'` is set in `src/main/index.js`, suppressing Electron's built-in console warnings about the above.

> [!WARNING]
> This is the **opposite** of the hardened Electron model (context isolation + sandbox + `contextBridge`). The mitigating factor is that Motrix loads only local, first-party content — it is not a browser for arbitrary web pages. The practical risk is concentrated in (a) the IPC command surface and (b) the engine RPC, below.

### 6.2 Engine RPC exposure

The bundled `aria2.conf` enables the RPC server with **`rpc-listen-all=true`** and **`rpc-allow-origin-all=true`**, and the default **`rpc-secret` is empty**.

- `rpc-listen-all=true` means aria2 listens on **all** network interfaces, not just loopback — even though Motrix itself connects via `127.0.0.1:16800` (`ENGINE_RPC_HOST`).
- `rpc-allow-origin-all=true` disables origin checks on RPC requests.
- An empty secret means no authentication.

> [!CAUTION]
> On a shared or untrusted LAN, any host that can reach TCP `16800` can issue aria2 RPC commands (add downloads, change global options, shut down the engine) **without authentication**. This is the substance of GitHub issue **#1795** ("RPC default listen address should be `127.0.0.1` not `0.0.0.0`"). **Mitigation:** set a non-empty **RPC Secret** in Preferences ▸ Advanced (system key `rpc-secret`), and/or firewall port `16800` to loopback. Administrators deploying Motrix to managed fleets should ship a `system.json` with a strong `rpc-secret` and consider patching the bundled `aria2.conf` to `rpc-listen-all=false`.

### 6.3 IPC trust boundary

The `command`/`event` IPC channels re-emit any string the renderer sends on the main-process EventEmitter (`Application.handleIpcMessages()`). Registered commands include `application:open-external` (which calls `shell.openExternal`). Because the channel is not action-allow-listed, a compromised or malicious renderer context could invoke any registered command — the basis of GitHub **#1796**.

> [!IMPORTANT]
> For a local, first-party-only desktop app this boundary is low-risk in normal use, but it means **renderer compromise = main-process command execution**. Hardening would require migrating to `contextBridge` with an explicit, validated action allow-list. This is noted in [§12 Potential Improvements](#12-potential-improvements).

### 6.4 Auto-update integrity

`UpdateManager` uses `electron-updater` against two publish providers (generic `https://dl.motrix.app/releases/` and GitHub). Updates are **not** auto-downloaded (`autoDownload = false`) — the user is prompted before download and before install (`autoInstallOnAppQuit = false`). The Mac App Store build (`is.mas()`) skips the updater entirely. Proxy settings are honored via `updater.netSession.setProxy`.

### 6.5 What does NOT exist (negative knowledge)

To prevent fabrication of imaginary attack/config surfaces, the following are confirmed **absent** in this codebase:

- **No HTTP API, no webhooks, no telemetry/analytics backend.** Motrix makes outbound requests only for tracker lists, update checks, and downloads you initiate.
- **No multi-user / account system, no remote management plane.**
- **No system-wide config file** (`/etc/motrix`, registry policy keys) — all config is per-user `electron-store` JSON.
- **No code-signing/notarization in the committed config:** `electron-builder.json` sets macOS `hardenedRuntime: false`, `notarize: false`, and `type: "development"`. Production release signing/notarization is handled by the maintainer's CI environment (`@electron/notarize`, `@electron/osx-sign` are present as dev deps), not by the committed config.
- **No Electron fuses** (`@electron/fuses` is not used).

---

## 7. Operations

### 7.1 Filesystem layout (per-user data)

Paths are resolved in `src/main/utils/index.js`. The base is `app.getPath('userData')` (or `PORTABLE_EXECUTABLE_DIR` in portable mode):

| File | Path helper | Purpose |
|------|-------------|---------|
| `system.json` | `getConfigBasePath()` | aria2 engine config store |
| `user.json` | `getConfigBasePath()` | App preference store |
| `download.session` | `getSessionPath()` | aria2 saved session (tasks restored on launch) |
| `engine.pid` | `getEnginePidPath()` | PID of the running `aria2c` process |
| `dht.dat` / `dht6.dat` | `getDhtPath()` | DHT routing tables (IPv4/IPv6) |
| `main.log` | electron-log | App log (portable: alongside executable) |

The bundled engine lives at `getEnginePath()` → in production `resolve(app.getAppPath(), '../engine')`; the binary name is platform-specific (`aria2c` / `aria2c.exe`) and the config is `aria2.conf` in the same folder.

> [!NOTE]
> Typical `userData` locations: Windows `%APPDATA%\Motrix`, macOS `~/Library/Application Support/Motrix`, Linux `~/.config/Motrix`. The exact path is whatever Electron returns for the `app.motrix.native` app ID.

### 7.2 Engine lifecycle

| Operation | Mechanism |
|-----------|-----------|
| **Start** | `Engine.start()` → `spawn(aria2c, args)` with `--conf-path`, `--save-session`, and `--input-file` (if a session exists), plus `transformConfig(systemConfig)` flags. Writes `engine.pid`. |
| **Stop** | `Application.stopEngine()` → `EngineClient.shutdown({force:true})` (RPC `forceShutdown`), then `Engine.stop()` kills the child and clears the PID file. |
| **Restart** | `Engine.restart()` = stop + start. Triggered by `needRestartKeys` changes. |
| **Reset session** | `application:reset-session` → stop engine, clear recent docs, delete `download.session`, restart after 3s. |
| **Factory reset** | `application:factory-reset` → detach config listeners, `configManager.reset()` (clears both stores), relaunch. |

### 7.3 Logs

`electron-log` writes to `main.log`. The file level is set from the `log-level` user pref (default `warn`; valid levels: `error`, `warn`, `info`, `verbose`, `debug`, `silly`) and updated live when the pref changes. In production the base level is `info`; in dev it's `silly`. The log path is surfaced read-only in Preferences ▸ Developer ▸ **App log path**.

### 7.4 Process supervision & power

- **Single instance:** `Launcher.makeSingleInstance()` uses `app.requestSingleInstanceLock()` (skipped for the Mac App Store sandbox). A second launch forwards its argv (URL/file) to the running instance via `second-instance`.
- **Energy:** `EnergyManager` starts a `powerSaveBlocker('prevent-app-suspension')` while any download is active and stops it when idle, so downloads aren't suspended by sleep.
- **Crash safety:** aria2 saves its session every 10s (`save-session-interval=10`) and control files every 10s (`auto-save-interval=10`), so an abrupt exit loses at most ~10s of progress metadata.

### 7.5 Port mapping (UPnP/NAT-PMP)

`UPnPManager` (via `@motrix/nat-api`, `autoUpdate:true`) maps the BT `listen-port` and `dht-listen-port` when `enable-upnp` is true. Port changes re-map automatically; disabling UPnP unmaps and closes the client. Mapping uses `Promise.allSettled` so a single failure (e.g. no UPnP-capable router) doesn't block startup.

### 7.6 Tracker auto-sync

When `auto-sync-tracker` is on and the interval (`AUTO_SYNC_TRACKER_INTERVAL` = 12 hours) has elapsed since `last-sync-tracker-time`, Motrix fetches the configured `tracker-source` lists, converts them to a comma string, reduces them to fit `MAX_BT_TRACKER_LENGTH` (6144 — guards against Windows `spawn ENAMETOOLONG`), and writes the result to `bt-tracker`.

---

## 8. Build & Distribution

### 8.1 Build scripts (`package.json`)

| Script | Action |
|--------|--------|
| `yarn dev` | `node .electron-vue/dev-runner.js` — dev with HMR |
| `yarn build` | `node .electron-vue/build.js && electron-builder` — full release build |
| `yarn build:applesilicon` | Build arm64 mac |
| `yarn build:dir` | Unpacked dir build (no installer) |
| `yarn pack` / `pack:main` / `pack:renderer` | Webpack production bundles only |
| `yarn lint` / `lint:fix` | ESLint over `.js`/`.vue` in `src` |

`asar: true` packs the app; the engine binaries are shipped as **extraResources** (`./extra/{platform}/${arch}/` → app root), keeping the native `aria2c` outside the asar so it can be executed.

### 8.2 Distribution matrix (`electron-builder.json`)

| Platform | Targets | Arches |
|----------|---------|--------|
| **macOS** | `dmg`, `zip` | x64, arm64, universal |
| **Windows** | `nsis` (installer), `appx`, `zip`, `portable` | x64, ia32 |
| **Linux** | `AppImage`, `deb`, `rpm`, `snap` | x64, arm64, armv7l (rpm/snap x64 only) |

- **NSIS:** non-one-click, allows changing install dir; artifact `Motrix-Setup-${version}.exe`.
- **AppX:** identity `59744DrrOot.Motrix`, publisher `Dr_rOot` — for the Microsoft Store.
- **DMG:** custom window layout with an Applications-folder drop target.
- **macOS** entries set `darkModeSupport: true`, `category: public.app-category.utilities`.

### 8.3 Protocols & file associations

Registered in `electron-builder.json` and `ProtocolManager`:

| Protocol | Schemes | Default |
|----------|---------|---------|
| Motrix Protocol | `mo`, `motrix` | Always (internal) |
| Magnet Protocol | `magnet` | On |
| Thunder Protocol | `thunder` | Off |

File association: `.torrent` (MIME `application/x-bittorrent`, role Viewer). Linux additionally declares `mimeTypes` for bittorrent and `x-scheme-handler/magnet`.

The `mo://`/`motrix://` scheme routes through `ProtocolManager.handleMoProtocol()` → `src/main/configs/protocol.js` map → an application command with query-string args. Resource schemes (`http/https/ftp/magnet/thunder`) route to `application:new-task`.

### 8.4 Publish & update channels

```json
"publish": [
  { "provider": "generic", "url": "https://dl.motrix.app/releases/" },
  { "provider": "github" }
]
```

`update-channel` defaults to `latest`. In dev, `UpdateManager` points `updateConfigPath` at the repo's `app-update.yml`.

### 8.5 Build hooks

`afterPack: ./build/afterPackHook.js` and `afterSign: ./build/afterSignHook.js` run during packaging (e.g. for signing/notarization steps). These live in `build/` (not in the analyzed source set) and should be reviewed before trusting a release pipeline.

---

## 9. Module Reference (Internals)

### 9.1 Renderer aria2 client — `src/renderer/api/Api.js`

The renderer's `Api` class is the primary engine driver. On init it loads config via the `get-app-config` IPC invoke, camelCases the keys, then opens an `Aria2` JSON-RPC client to `127.0.0.1:<rpcListenPort>`. Selected methods (each maps to an aria2 RPC method):

| Method | aria2 RPC | Purpose |
|--------|-----------|---------|
| `addUri(uris, outs, options)` | `aria2.addUri` (multicall) | Add HTTP/FTP/magnet tasks |
| `addTorrent(torrent, options)` | `aria2.addTorrent` | Add a `.torrent` |
| `addMetalink(metalink, options)` | `aria2.addMetalink` | Add a metalink |
| `fetchDownloadingTaskList` | `tellActive` + `tellWaiting` (multicall, merged) | Downloading list |
| `fetchStoppedTaskList` | `tellStopped` | History |
| `fetchTaskItemWithPeers` | `tellStatus` + `getPeers` | Task detail + peers |
| `pause/resume/remove` (+ `force*`, `*All`) | `pause`/`unpause`/`remove`/… | Task control |
| `changeOption` / `changeGlobalOption` | `changeOption`/`changeGlobalOption` | Per-task / global options |
| `batch*` | `system.multicall` | Bulk operations over many GIDs |
| `saveSession` / `purgeTaskRecord` / `removeTaskRecord` | `saveSession`/`purgeDownloadResult`/`removeDownloadResult` | Session & history |
| `getGlobalStat` / `getVersion` | `getGlobalStat`/`getVersion` | Stats & engine version |

Options are normalized for the engine via `formatOptionsForEngine` (kebab-case keys, drop empties) and `compactUndefined`.

### 9.2 Vuex store — `src/renderer/store/modules/`

- **`task.js`** — the task lifecycle. State: `taskList`, `currentList` (active/waiting/stopped), `selectedGidList`, `currentTaskItem`, `seedingList`, detail visibility. Actions wrap the `Api`: `addUri/addTorrent/addMetalink`, `fetchList`, `pause/resume/toggleTask`, `removeTask/removeTaskRecord`, `batch*`, `stopSeeding` (sets `seedTime:0`). Every mutating action re-fetches the list and calls `saveSession`. BT tasks are force-paused (`checkTaskIsBT`).
- **`preference.js`** — config state + `save`/`fetchPreference`, history/favorite directory management (fixed-length, max 5), tracker fetch, theme/locale updates.
- **`app.js`** — global UI state (add-task dialog, engine mode, menu states).

### 9.3 Shared aria2 RPC lib — `src/shared/aria2/lib/`

A small, self-contained JSON-RPC client: `Aria2.js` (connection, `call`, `multicall`, open/close over websocket via `ws`), `JSONRPCClient.js`, `JSONRPCError.js`, `Deferred.js`, `promiseEvent.js`. This is what both `EngineClient` (main) and `Api` (renderer) build on.

### 9.4 Shared utilities — `src/shared/utils/`

`separateConfig` (split into user/system/others by key list), `formatOptionsForEngine`, `transformConfig` (config → CLI args), `changeKeysToCamelCase`/`changeKeysToKebabCase`, `mergeTaskResult`, `detectResource`, `checkTaskIsBT`, plus `tracker.js` (fetch/convert/reduce tracker lists), `rename.js`, `curl.js`, `tray.js`.

### 9.5 Localization — `src/shared/locales/`

25 locale folders, each with `about/app/edit/help/index/menu/preferences/subnav/task/window.js`. RTL languages (`ar`, `fa`, `he`, `ku`, `pa`, `ps`, `sd`, `ur`, `yi`) are handled via `SUPPORT_RTL_LOCALES` and `getLangDirection`. Menu accelerators are derived from `src/shared/keymap.json` and translated per-locale in `src/main/utils/menu.js`.

---

## 10. Code Quality & Patterns

**Strengths**
- **Clean manager decomposition.** The main process is a set of single-responsibility managers wired by `Application`, each owning one concern (config, engine, tray, updates, UPnP). Easy to reason about.
- **Reactive config.** `electron-store`'s `onDidChange`/`onDidAnyChange` drives live behavior (proxy, theme, locale, ports, tray) without restarts where possible; `needRestartKeys` cleanly delineates what does need a restart.
- **Defensive engine handling.** Missing-binary throws a localized error; `Promise.allSettled` everywhere port mapping or shutdown could partially fail; PID file cleanup on close; tracker-string reduction to dodge Windows arg-length limits.
- **Separation of config domains.** The user/system/others split with an explicit allow-list keeps unknown keys out of the engine.

**Conventions**
- ES modules, Babel-transpiled. ESLint with `@vue/eslint-config-standard`.
- Path aliases `@` (renderer), `@shared` (shared) configured in webpack/jsconfig.
- Heavy use of constants (`src/shared/constants.js`) rather than magic values.

**Weak spots / risks** (see also §12)
- Old Electron security posture (§6).
- The IPC channel is un-allow-listed (§6.3).
- A couple of `@TODO`s in the renderer (`addMetalink` is wired in the store/API but not exposed in the AddTask UI; `loadConfigFromLocalStorage` is a stub for non-renderer contexts).

---

## 11. Troubleshooting (Operational)

### Engine won't start / "engine bin is not exist"

`Engine.getEngineBinPath()` throws if the bundled `aria2c` is missing. Verify the `engine/` folder sits next to the app (`resolve(app.getAppPath(), '../engine')`) and that AV/endpoint-protection isn't quarantining `aria2c`. On Linux, run once with `sudo` so the session file under `/var/cache` can be created.

### Downloads don't restore after restart

Check that `download.session` exists in `userData` and that **Automatically resume all unfinished tasks** is enabled (it triggers `unpauseAll` on launch). A corrupt session can be cleared via **Reset download session**.

### RPC port conflict

If `16800` is taken, change **RPC Listen Port** (a `needRestartKeys` key — Motrix restarts the engine). Likewise `21301`/`26701` for BT/DHT.

### Memory growth over long sessions

A suspected leak is reported (GitHub #1798). Mitigate by periodically restarting the app for very long-lived sessions; keep to the latest release.

### Window won't show / app hidden in tray mode

In **Hide Tray Application** mode there's no tray indicator; relaunch the app (single-instance lock will surface the existing window) or quit via <kbd>Ctrl</kbd>+<kbd>Q</kbd>. macOS dock visibility follows `run-mode` and full-screen transitions (`DockManager`).

### Build: native module ABI mismatch

If the app crashes after a dependency change, re-run `yarn` so `postinstall`'s `electron-builder install-app-deps` rebuilds native modules (`@motrix/nat-api`) against Electron's ABI.

---

## 12. Potential Improvements

These are derived from the code and the issue tracker — candidate work for a maintainer or fork (e.g. the community "Motrix Next" rewrite noted in #1805).

1. **Harden the Electron security model.** Migrate to `contextIsolation: true` + `sandbox: true` + `contextBridge` with a validated action allow-list; drop `@electron/remote`. Directly addresses #1796.
2. **Lock down the engine RPC.** Patch the bundled `aria2.conf` to `rpc-listen-all=false`, generate a random `rpc-secret` on first run, and bind to `127.0.0.1` only. Addresses #1795.
3. **Adopt Electron fuses** (`OnlyLoadAppFromAsar`, disable `RunAsNode`/inspect args) at build time.
4. **Modernize the stack.** Vue 2.7 and Element UI 2 are EOL; Electron 22 is past end-of-life. A Vue 3 + maintained-component-library + current-Electron migration is the largest single risk reduction.
5. **Expose `addMetalink` in the UI** (already wired in store/API).
6. **Multi-select task list** as a first-class feature (#1766) and per-task download-path changes for active tasks (#1794).
7. **Investigate the suspected memory leak** (#1798) and the Edge-extension double-download (#1516).
8. **Portable-mode data isolation** — ensure *all* state stays in the portable folder (#1016).

> [!NOTE]
> Development has slowed and the maintainer's status is discussed in #1396/#1807. Treat this codebase as stable-but-quiet: dependable for use, but expect to fork for deep changes.

---

## 13. Quick Reference

### Ports & endpoints

| Item | Default |
|------|---------|
| RPC host:port | `127.0.0.1:16800` |
| BT listen port | `21301` |
| DHT listen port | `26701` |
| RPC secret | *(empty — set one)* |

### Key files (per-user data dir)

| File | Purpose |
|------|---------|
| `system.json` | aria2 engine options |
| `user.json` | app preferences |
| `download.session` | restorable task session |
| `engine.pid` | aria2 process PID |
| `dht.dat` / `dht6.dat` | DHT tables |
| `main.log` | app log |

### Application commands (selected, IPC `command` channel)

`application:new-task`, `application:new-bt-task`, `application:open-file`, `application:save-preference`, `application:reset-session`, `application:factory-reset`, `application:check-for-updates`, `application:change-theme`, `application:change-locale`, `application:relaunch`, `application:quit`, `application:open-external`, `application:reveal-in-folder`, `application:setup-protocols-client`.

### Engine restart-triggering keys

`dht-listen-port`, `hide-app-menu`, `listen-port`, `rpc-listen-port`, `rpc-secret`.

### Enums

| Enum | Values |
|------|--------|
| Run mode | Standard (1), Tray (2), Hide Tray (3) |
| Theme | auto, light, dark |
| Task status | active, waiting, paused, error, complete, removed, seeding |
| Proxy scope | download, update-app, update-trackers |
| Add task type | uri, torrent |

### Build commands

```bash
yarn            # install + rebuild native deps
yarn dev        # development with HMR
yarn build      # full electron-builder release
yarn lint:fix   # lint and auto-fix
```

---

_Generated June 2026 from agalwood/Motrix at 7012040. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
