Before creating the plugin market, there was first the MoeKoe standard plugin system.
At that time, the problem I wanted to solve was not a specific function, but a more general one: there would always be many small needs in the player, some people wanted to change the interface, some people wanted to add a tool popup, and some people wanted to connect their own services. If each idea is stuffed into the main program, the main program will become heavier and harder to maintain.
So I initially set a clear direction for the plugin system: the main program only provides the plugin running environment and management entry, and the specific functions are handled by the plugins themselves. Plugins can be independently installed and uninstalled, and it is best not to affect the player itself when they break.
In the previous article, I wrote about the MoeKoeMusic-Plugins
plugin registration repository. There, the problem of “how to generate the data of the plugin market” was solved: users submit issues, Actions validate, maintainers review, and after approval, they are written into plugins.json.
But having a registration repository is not enough. To users, the plugin market is not just a JSON on GitHub, but an entry in the client that can be viewed, searched, installed, and updated.
So this article shifts to the MoeKoe Music main project, focusing on the plugin market part of the client.
Why Choose Chrome Extension
MoeKoe is an Electron application, and Electron itself has the ability to load extensions. Since the underlying technology can run Chrome Extensions, there is no need to design a new plugin format from scratch.
This approach has several advantages:
- First, plugin authors do not need to learn a completely unfamiliar format.
manifest.json,popup.html,background.jsare all common concepts in browser extensions. - Second, plugins naturally have a manifest file. The main program can first read
manifest.jsonto determine the plugin name, version, description, permissions, popup entry, and minimum supported version. - Third, Electron can directly load the plugin directory without the need for me to write a script sandbox.
Display, Search, Pagination, and Status
The plugin market is not just about being able to install, but also about being able to quickly judge:
- What this plugin does
- Whether I have installed it
- Whether I have an outdated version installed
- Whether this plugin has higher version requirements
- Whether it has risks such as network access, file access, and local binary
- Pagination, search, updates, status
Therefore, the market card does not only show the “install” button.
- Ensure the plugin directory exists
- Call Electron to download, install, uninstall the old version, and install the new version
- Register IPC related to plugins
- Load Chrome extensions
- Load Native Host index and authorization system
Installed Plugins
The “Installed Plugins” in the plugin management page is not just a read-only list of Electron currently loaded extensions. It merges two types of information:
session.defaultSession.getAllExtensions()to get the loaded extensionsscanExtensions()scans the disk plugin directory, reads manifest, icon, popup, and Native Host declarations
After the front-end gets these fields, it can display:
- Plugin name, description, version, author
- Icon
- Whether it is a MoeKoe compatible plugin
- Whether the current client version is lower than the plugin
minversion - Whether there is a popup settings page
- Whether it declares local binary programs
Native Host
This is a new plugin feature added in version 2.0.
The most careful part of the plugin system is Native Host, which is the ability of the plugin to attach local executable programs.
No matter how much a regular front-end plugin messes around, most of them are still within the browser extension model. But local programs are different. Once started, they have system access capabilities like ordinary desktop applications.
Therefore, MoeKoe Music does not allow plugins to run directly after declaring, but designs a layer of authorization.
The manifest must declare permissions: 'moekoe:nativeHost'
Local Program Communication
When the Native Host is actually started, it uses spawn:
const child = spawn(host.executablePath, host.args, {
cwd: path.dirname(host.executablePath),
stdio: ['pipe', 'pipe', 'pipe']
});
Here, shell is not used, and this choice is very important. It avoids the plugin getting extra command execution capabilities by concatenating parameters.
When the plugin sends a message to the local program, it writes a line of JSON:
child.stdin.write(`${JSON.stringify(payload)}\n`, error => {
// ...
});
This is the JSON Lines protocol: one JSON per line.
The local program parses stdout line by line, and the message size limit is 64 KB
This protocol is not complex, but it is clear enough. Plugins and local programs do not need to share a lot of state, just agree on input and output JSON.
Bridge Window
There is also an interesting design in the Native Host: bridge, which gives the plugin background a channel to preload.
The code comments are very clear:
// bridge is a hidden extension page that can access chrome.runtime and also access electronAPI through preload.
// background/service worker cannot directly access Electron preload, so this intermediate page is needed.
In practice, a 1×1 hidden BrowserWindow is created
Why do this?
Because Chrome Extension’s background/service worker may not be able to directly access Electron preload exposed API. And bridge is an extension page, it belongs to chrome-extension://, and runs in an Electron window, so it can act as an intermediate layer.
The Last Question
After reading this, you may have a question: since MoeKoe Music has already used the Chrome Extension model, why doesn’t the Native Host communication directly follow the Chrome browser’s built-in Native Messaging?
The Chrome Native Messaging mechanism itself is mature. Browser extensions can connect to local programs through chrome.runtime.connectNative or chrome.runtime.sendNativeMessage, and local programs and browsers communicate with stdin/stdout, with message format being “4-byte length header + JSON content”.
But the problem MoeKoe Music needs to solve is not entirely the same.
Chrome Native Messaging is aimed at “browser extensions connecting to native hosts that have already been registered in the system”. It requires local programs to first register host manifest in a specific location in the operating system, such as a fixed directory on macOS/Linux, or the registry on Windows. This manifest also needs to write allowed_origins, listing the Chrome extension ids that are allowed to access it.
First, MoeKoe’s plugins are installed from the client market to the application’s own plugin directory. Clicking to install a plugin is essentially just writing extension files to MoeKoe Music’s user data directory. If you need to change the system-level native host registration information for a local program in a plugin, installation, update, and uninstallation will become more complex. When uninstalling, the client must also ensure that the system registration information is also cleared, otherwise it will leave residual states outside the application.
Second, Chrome’s authorization is based on browser extension id and host manifest, while MoeKoe Music needs to be managed around plugin_id. As mentioned earlier, the extension id generated by Electron is more like a runtime identity, and plugin market, review records, update judgment, and Native Host authorization all need a more stable identity. If it directly goes through Chrome Native Messaging, the permission chain will be split into system host manifest and browser extension id, which is not well aligned with MoeKoe’s own plugin market trust chain.
Third, MoeKoe Music needs the main process to have complete control over the lifecycle. Who can start the local program, whether it has been authorized before starting, whether the executable file is a relative path in the plugin directory, whether to close the subprocess when exiting the application, and how to clean up when the plugin is updated or uninstalled, these judgments should be concentrated in the Electron main process. Spawning local programs yourself can put all these rules inside the client, rather than giving part of the state to the operating system registration mechanism.
Fourth, Chrome Native Messaging’s length header protocol is very general, but it is not necessary for this scenario. MoeKoe’s Native Host is more like a command channel between plugins and local small tools, with small message volume and no need to transmit binary streams. Using JSON Lines, one JSON per line, is very easy for local program authors to implement with Node, Python, Go, Rust, and it is also more intuitive to debug by directly looking at stdout.
Fifth, Electron does not support chrome.runtime.connectNative or chrome.runtime.sendNativeMessage, these two APIs are Chrome browser extension (Extension) Native Messaging API, which depends on the Native Messaging Host installed in the browser, rather than the Chromium kernel itself, so Electron does not implement this mechanism. So much has been said, and this is the main reason
Conclusion
If you only look at the interface, MoeKoe Music’s plugin market is just a list: icons, names, descriptions, versions, buttons.
But if you look inside, it is actually a trust chain:
Plugin authors submit to the registration repository, maintainers review snapshots, Actions write to plugins.json, the client reads the market index, users see permission prompts, click to install, the main process downloads and verifies the zip, and finally Electron loads the extension.
- Where do plugins come from
- Which version is approved
- How does the client know if it can be updated
- How does the installation package fall to the local
- How is local binary capability authorized
- How to clean up when plugins are uninstalled or the application exits
Finally, the user sees is just a “install” button.
But behind this button, a whole set of very specific and engineering judgments have already been made.
