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.
Table of Contents
- Executive Summary
- Architecture
- System Requirements
- Configuration Mastery
- The aria2 Engine Option Reference
- Security Model
- Operations
- Build & Distribution
- Module Reference (Internals)
- Code Quality & Patterns
- Troubleshooting (Operational)
- Potential Improvements
- Quick Reference
1. Executive Summary
Motrix is an Electron desktop application that wraps the aria2 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/remoteenabled, no context isolation, directipcRendereruse). 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 — 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:
- Main process → engine (
EngineClient) — used for global option changes, shutdown, and resume-on-launch. - Renderer → engine (
api/Api.js) — the primary path: add/pause/resume/remove tasks, poll status, fetch peers. - 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
commandchannel 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 arbitraryshell.openExternaland internal command invocation"). See §6.3.
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 thepackage.jsonscripts. Thepostinstallstep runselectron-builder install-app-depsto 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.jsonoverrides the built-inaria2.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() | |
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=falsedisables 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 viasystem.json(check-integrity/checksumare 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 — 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/remoteis enabled for every window (Launcher.handleRendererRemote()callsrequire('@electron/remote/main').enable(window.webContents)), andinitialize()runs insrc/main/index.js.- The renderer uses Node/Electron APIs directly (
ipcRenderer,electron-is), indicatingnodeIntegration: trueand nocontextIsolation(the hardenedcontextBridgepattern is not used). process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'is set insrc/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=truemeans aria2 listens on all network interfaces, not just loopback — even though Motrix itself connects via127.0.0.1:16800(ENGINE_RPC_HOST).rpc-allow-origin-all=truedisables origin checks on RPC requests.- An empty secret means no authentication.
Caution
On a shared or untrusted LAN, any host that can reach TCP
16800can 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 be127.0.0.1not0.0.0.0"). Mitigation: set a non-empty RPC Secret in Preferences ▸ Advanced (system keyrpc-secret), and/or firewall port16800to loopback. Administrators deploying Motrix to managed fleets should ship asystem.jsonwith a strongrpc-secretand consider patching the bundledaria2.conftorpc-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
contextBridgewith an explicit, validated action allow-list. This is noted in §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-userelectron-storeJSON. - No code-signing/notarization in the committed config:
electron-builder.jsonsets macOShardenedRuntime: false,notarize: false, andtype: "development". Production release signing/notarization is handled by the maintainer's CI environment (@electron/notarize,@electron/osx-signare present as dev deps), not by the committed config. - No Electron fuses (
@electron/fusesis 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
userDatalocations: Windows%APPDATA%\Motrix, macOS~/Library/Application Support/Motrix, Linux~/.config/Motrix. The exact path is whatever Electron returns for theapp.motrix.nativeapp 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()usesapp.requestSingleInstanceLock()(skipped for the Mac App Store sandbox). A second launch forwards its argv (URL/file) to the running instance viasecond-instance. - Energy:
EnergyManagerstarts apowerSaveBlocker('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, publisherDr_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
"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 theApi:addUri/addTorrent/addMetalink,fetchList,pause/resume/toggleTask,removeTask/removeTaskRecord,batch*,stopSeeding(setsseedTime:0). Every mutating action re-fetches the list and callssaveSession. 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'sonDidChange/onDidAnyChangedrives live behavior (proxy, theme, locale, ports, tray) without restarts where possible;needRestartKeyscleanly delineates what does need a restart. - Defensive engine handling. Missing-binary throws a localized error;
Promise.allSettledeverywhere 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
@TODOs in the renderer (addMetalinkis wired in the store/API but not exposed in the AddTask UI;loadConfigFromLocalStorageis 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 Ctrl+Q. 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).
- Harden the Electron security model. Migrate to
contextIsolation: true+sandbox: true+contextBridgewith a validated action allow-list; drop@electron/remote. Directly addresses #1796. - Lock down the engine RPC. Patch the bundled
aria2.conftorpc-listen-all=false, generate a randomrpc-secreton first run, and bind to127.0.0.1only. Addresses #1795. - Adopt Electron fuses (
OnlyLoadAppFromAsar, disableRunAsNode/inspect args) at build time. - 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.
- Expose
addMetalinkin the UI (already wired in store/API). - Multi-select task list as a first-class feature (#1766) and per-task download-path changes for active tasks (#1794).
- Investigate the suspected memory leak (#1798) and the Edge-extension double-download (#1516).
- 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
yarn # install + rebuild native deps
yarn dev # development with HMR
yarn build # full electron-builder release
yarn lint:fix # lint and auto-fix