How the package manager works
The package manager is part of LonelyIce.exe. The launcher's Plugins page and LonelyIce --pkg
(Command line) run the same code. It reads catalogs, resolves versions and
dependencies, and installs, updates, enables, disables and removes plugin folders. It only changes files in the
plugins folder. The databases and the game client catch up on the next server start (or --pkg apply). The
package and index formats are in Plugin format.
Catalogs
A catalog is one index.json. The catalogs in use are [packages] index in lonelyice.ini (lonelyice.ini),
and [packages] disabled holds the ones kept but not read. Both lists are separated by ;, and spaces and tabs
around each entry are trimmed. The default is https://lonelyice.org/packages/index.json. An old
https://raw.githubusercontent.com/LonelyIceProject/packages/main/index.json entry is replaced with it when the
settings are read. --pkg --index <catalogs> replaces the list for one command.
| Location | Read as |
|---|---|
http://…, https://… |
Downloaded with libcurl. |
file://… |
A local file (file:///C:/dir/index.json on Windows). |
| A path to a file | That file. |
| A path to a folder | <folder>/index.json. |
Downloads accept only http and https, including after redirects, and follow up to 10 redirects. Certificate
checks are on. The connect timeout is 15 seconds, and a transfer is aborted when less than 1 byte per second
arrives for 60 seconds. An HTTP status of 400 or higher is an error.
When you add a catalog on the Plugins page, the launcher refuses:
- a location containing
;; - a local path that does not exist;
- a folder without
index.json; - a location already in the list.
Reading a catalog. The document must have a packages array. Without one the catalog fails with
"not a package index". name (localized) is shown in the catalog list. format is not checked. An entry is kept
only when all of these hold:
id,versionandurlare present.- If the entry has
platforms: itscoreequals this build's ABI and itsplatformsinclude this system (see Platforms and core ABI). An entry withoutplatformsis kept whatever itscoreis.
url, icon and page are resolved against the index location. Values that start with http://, https:// or
file://, and absolute paths, are used as they are. Anything else is appended to the index location, cut after its
last / or \.
Several catalogs. Catalogs are read in list order. The entries of all readable catalogs go into one list, in catalog order and then entry order, with nothing merged or removed. The same id can therefore appear several times, from one catalog or several:
- The Plugins page shows the newest version of each id. When versions are equal, the entry read first wins. With more than one catalog, each row names its catalog.
- The resolver takes the newest version that fits. When versions are equal, the entry read first wins, so list order only breaks ties. A later catalog with a higher version still wins. You cannot tie a package to a catalog.
- Dependencies are resolved from all catalogs together.
Failures. A catalog that cannot be read is skipped. --pkg prints skipped index <location>: <error>, and the
Plugins page marks it in the catalog list and the events. Loading as a whole fails only when every catalog fails
(error: package index: …). An empty list loads nothing and is not an error.
When catalogs are read. The launcher reads them when it starts or when the Plugins page first opens, on
Check for updates, and after the catalog list changes. --pkg available, install and update read them on
every run. list, remove, enable, disable, apply and pack do not read them.
Version comparison
The launcher (Manager::CompareVersions) compares versions like this:
- It splits the version at
.and reads each part's leading digits as a number. A part without leading digits counts as 0. - It compares the first three parts numerically. Missing parts count as 0, and parts after the third are ignored.
| Comparison | Result |
|---|---|
1.10.0 vs 1.9.0 |
1.10.0 is newer |
1.2 vs 1.2.0 |
equal |
1.2.3-beta vs 1.2.3 |
equal (3-beta reads as 3) |
1.2.3.4 vs 1.2.3 |
equal |
v1.2.0 vs 0.2.0 |
equal (v1 reads as 0) |
The launcher accepts any version string. The LonelyIce catalog accepts only x.y.z with digits
(What the catalog checks).
Version ranges
depends values, and install <id>@<range>, are ranges. They mean what they mean in npm's semver. The launcher's
resolver and the server's plugin loader use the same code (Acore::VersionRange in the core, which
PluginMgr::Satisfies calls):
- A range is a list of comparators separated by spaces and/or commas, and a version must satisfy every one of them:
>=1.0.0 <2.0.0,>=1.0.0,<2.0.0and>=1.0.0, <2.0.0are the same range. An empty range,*orxmatches everything. - A comparator is an optional operator (
=,<,<=,>,>=,~,^;~>is read as~) and a version. An operator followed by a space takes the next word as its version (>= 1.2.0). - The version has one to three numeric parts (
1,1.2,1.2.3), may start withv, and may end in+buildmetadata, which is ignored.x,Xor*stands for a part and every part after it (1.x,1.2.*). - The version being checked is read as before: the leading digits of its first three parts, missing parts 0 (Version comparison).
| Comparator | Matches |
|---|---|
*, x |
any version |
1.2.3, =1.2.3 |
exactly 1.2.3 |
1.2, 1.2.x, =1.2 |
>=1.2.0 <1.3.0 |
1, 1.x |
>=1.0.0 <2.0.0 |
>=1.2.0, >=1.2 |
1.2.0 and newer |
>1.2.0 |
newer than 1.2.0 |
>1.2 |
>=1.3.0 (newer than every 1.2.x) |
<2.0.0, <2 |
older than 2.0.0 |
<=1.2.0 |
1.2.0 and older |
<=1.2 |
<1.3.0 (every 1.2.x included) |
~1.2.3 |
>=1.2.3 <1.3.0 |
~1.2 |
>=1.2.0 <1.3.0 |
~1 |
>=1.0.0 <2.0.0 |
^1.2.3 |
>=1.2.3 <2.0.0 |
^1.2 |
>=1.2.0 <2.0.0 |
^0.2.3 |
>=0.2.3 <0.3.0 |
^0.0.3 |
>=0.0.3 <0.0.4 |
^0.2, ^0.0 |
>=0.2.0 <0.3.0, >=0.0.0 <0.1.0 |
^1, ^0 |
>=1.0.0 <2.0.0, >=0.0.0 <1.0.0 |
>*, <* |
nothing |
Anything else is an error, never a partial reading: alternatives (||), hyphen ranges (1.0.0 - 2.0.0),
pre-release versions (1.2.3-beta), other operators (==, =>, !=), a fourth part, a number after a wildcard
(1.x.3). The package manager then stops with the version range "<range>" for <id> (<who>) cannot be read at "<part>": …: for a request before resolving; for a depends of a catalog entry or an installed plugin when
resolving fails on it (such an entry can never be chosen, so an older one that fits is taken instead); on enable;
and on pack. The server skips a plugin with such a range in depends
(cannot read the version range …).
Resolving an install or an update
The resolver takes requests (id → range), an update flag, the installed plugins and the catalog entries. The
installed plugins are the folders in plugins/ and plugins/.disabled/ whose plugin.json has an id.
| Command | Requests | Update flag |
|---|---|---|
--pkg install <id>[@<range>]... |
each id with its range, default * |
off |
--pkg update <id>... |
each id with * |
on |
--pkg update, Update all |
every enabled installed plugin that has a newer version in any catalog, with * |
on |
| Install on a catalog row | the id with * |
off |
| Update on an installed row | the id with * |
on |
The resolver searches for a set of choices, one per id it has to decide, that satisfies every range in play. It works depth first and takes a choice back when it leads to a dead end (backtracking), so a version that turns out not to fit is replaced together with the ranges it brought:
- Ranges in play always come from the current choices only:
- each requested id's range, recorded as "requested";
- the
dependsof every enabled installed plugin that stays: not touched by the request, or kept in its installed version; - the
dependsof every chosen catalog package. An installed plugin that is replaced brings its new version's ranges, no longer the old ones.
- Ids to decide are the requested ids and, repeatedly, the dependencies of what has been decided. They are decided in id order.
- Candidates for an id, tried in this order, each only if it satisfies every range in play for that id:
- without the update flag for that id: the installed version (kept), then the catalog versions from the newest
down. A catalog version can be older than the installed one (
install [email protected]downgrades, the step showsfrom -> to); - with the update flag (
update, Update): the catalog versions newer than the installed one from the newest, then the installed version, then older catalog versions. Catalog entries with the installed version are the installed copy and are not offered again. When versions are equal, the entry read first comes first.
- without the update flag for that id: the installed version (kept), then the catalog versions from the newest
down. A catalog version can be older than the installed one (
- Disabled plugins never satisfy anything. The server does not load them. If an id to decide is a disabled
plugin (requested or needed), that branch fails with
<id> is disabled: enable it first (needed by: …); runenablefirst (which checks the plugin's own dependencies, see below). - Conflicts are checked on a complete set of choices, in both directions, among what will be enabled
afterwards (the enabled installed plugins and the chosen packages):
- a chosen package that lists an enabled plugin or another chosen package in its
conflicts:<id> conflicts with <other>; - an enabled installed plugin that stays and lists a chosen package in its
conflicts(read from itsplugin.json):<id> cannot be installed: the installed plugin <other> conflicts with it. A conflict is a dead end like any other, so another version is tried.
- a chosen package that lists an enabled plugin or another chosen package in its
- Failure. When no set of choices works, the error of the deepest dead end is shown, usually
no suitable version of <id>: needs <range> (<who>), …or one of the above. The search stops after 20,000 steps withthe dependencies could not be resolved: too many version combinations. - Order. Chosen catalog packages become steps with dependencies first. A plugin kept in its installed version
gets no step. With no steps, the result is "nothing to install" (
--pkg) or "all installed" (launcher).
Only ids in play are decided: an installed plugin that nothing in the request touches stays as it is, even if its
own dependencies are already broken. update without ids requests only enabled plugins that have a strictly newer
version (see Version comparison); a rebuilt package with the same version is never offered.
Platforms and core ABI
| Value | Where it comes from |
|---|---|
| This build's ABI | AC_PLUGIN_ABI of the core LonelyIce was built with; lonelyice-ac-2 for current releases (Plugin API). |
| This build's platform | AC_PLUGIN_PLATFORM: windows-x64, linux-x64, linux-arm64, macos-x64, macos-arm64 (Plugin API). |
Entries that fail the catalog filter (Catalogs) are neither shown nor chosen. A dependency that exists
only for another ABI or platform ends in "no suitable version". The package manager does not check installed
plugins against the ABI. The server does that when it loads them: it compares core.abi with its own ABI, and the
library's exported ABI and platform with its own. A plugin that fails is not loaded, and neither are the plugins that
depend on it (Plugin API).
Languages
Index entries carry the locales of the manifest (Plugin format). A package matches a
language when its locales list that code or *.
| Where | Filter |
|---|---|
| Plugins page, Catalog view | Language (saved as [packages] locale). Empty: all packages. |
--pkg available --locale <code> |
Only matching packages. |
A package without locales matches no language, so a language filter hides it. The filter does not apply to the
Installed and Updates views or to dependency resolution. Names and descriptions from the index are shown in
the launcher's language, then en, then any language.
Download and verification
An install runs in two phases. Nothing installed changes until every package of the plan has been downloaded, checked and unpacked.
Before anything, the install is refused while a server runs on the plugins folder (see
Enable, disable, remove). plugins/.staging is emptied. Then, for each step in order,
dependencies first, the package manager:
- Downloads
url(http(s) or local, as for catalogs). - Checks
sizeif the entry has a non-zero one: the download must have exactly that many bytes. - Checks
sha256if the entry has one: the SHA-256 of the download in hex must equal it; upper and lower case are the same. - Unpacks the zip into
plugins/.staging/<id>.- The zip must have
plugin.jsonat its root, or in one top folder of any name. With a top folder, entries outside it are ignored. - An entry with an absolute path, a
:or a..part stops the install withinvalid path in the package: <name>. - A zip without
plugin.jsonstops withthe package has no plugin.json.
- The zip must have
- Checks that the unpacked
plugin.jsonhas the entry'sidandversion. Otherwise the install stops with<id>: the package's plugin.json does not match the index.
A failed check stops the install with <id>: the package is damaged (size or sha256 mismatch). If the entry has
neither field, nothing is checked. Any failure in this phase deletes plugins/.staging and leaves the plugins as
they were.
Installing
When every package is staged, the package manager replaces the folders, step by step:
- Everything that is in the way is moved (renamed) into
plugins/.backup/<time>/: the installed copy of the plugin, enabled or disabled, and whatever else is atplugins/<id>. If that fails:<id>: could not move the old version aside, is the server running? (…). - The staged folder is moved to
plugins/<id>. If that fails:<id>: could not install (…).
Every move is recorded. When one fails, all moves made so far, of this step and the earlier ones, are undone in
reverse order: the new folders go back to .staging, the old ones back where they were, and the error ends with
"Nothing was changed: the previous versions are back in place." If a move cannot be undone, the error lists the
folders to move by hand (<from> > <to>), and plugins/.backup/<time> is kept. After the last step, the
backups and plugins/.staging are deleted and the installed and updated plugins are listed. Moves are renames
inside the plugins folder, so they do not copy anything; on Windows a folder with a loaded library cannot be
renamed, which makes a running server fail step 1 before anything changed.
The new version is always enabled (plugins/<id>). Files outside the plugin folder are never touched: the plugin's
configs/modules/<name>.conf stays through updates and removal.
Enable, disable, remove
| Action | Effect | Refused when |
|---|---|---|
disable <id> |
Moves plugins/<folder> to plugins/.disabled/<folder>. The server does not look into .disabled. |
An enabled plugin needs it, directly or through others. |
enable <id> |
Moves it back. | One of its dependencies is not installed, is disabled or is out of range; a range of its depends cannot be read; it conflicts with an enabled plugin, or an enabled plugin conflicts with it. |
remove <id> |
Deletes the plugin folder, enabled or disabled. | An enabled plugin needs it, directly or through others. |
A refusal names the dependents: <id> is required by: … (--pkg) or <name> is needed by: …. (launcher). A
refused enable names everything that is missing:
<id> cannot be enabled, it needs: <dep> <range> (not installed), <dep> (disabled), <dep> <range> (installed: <version>),
or <id> conflicts with <other>. Enable the dependencies first, dependencies of dependencies before them.
While the server runs nothing changes. The server process (LonelyIce --server, also --apply and
--deploy) holds plugins/.cache/server.lock open and locked for as long as it runs; the operating system drops the
lock when the process ends, however it ends. While the lock is held:
--pkg install,update,remove,enable,disableandapplystop at once withthe server is running on this plugins folder (<folder>): stop it first;- the package manager's install, remove, enable and disable refuse with the same message, so the launcher does too;
- the Plugins page refuses every change while its own server runs, before that ("Stop the server: plugins change while it is off.").
list, available and pack work while the server runs.
Removing or disabling a plugin does not undo its SQL updates: its tables and rows stay in the databases. Its patch recipes are undone on the next start (see below); a disabled plugin keeps its named ids, a removed one loses them.
On the next server start
The server (LonelyIce --server, started by the launcher) runs these steps in order:
- Load plugins. The server reads
plugins/, checks dependencies, conflicts and ABI, and loads the libraries (Plugin API). - Open the databases.
- Plugin SQL. The server hashes the core database connection strings (
LoginDatabaseInfo,CharacterDatabaseInfo,WorldDatabaseInfo) and, for every.sqlfile in thedatabasesfolders (auth,characters,world) of every loaded plugin, the plugin id, the plugin version, the database, the file's relative path and the SHA-256 of its contents. It compares the hash withplugins/.cache/sql.stamp.- When they differ and
Updates.EnableDatabasesis off (the normal case after the wizard), the core's updater runs over the loaded plugins' folders only. When it is on, the updater has already applied them. - The new stamp is saved afterwards.
- If the updater fails, the server does not start (
@@LI state failed database). - An edited SQL file changes the stamp, so the updater runs; what it does with a file already recorded in the
database's
updatestable under another hash follows the core's rules (Updates.Redundancy). - Databases a plugin owns (an object in
databases, such as playerbots' own database) are not covered: the plugin opens and updates them itself on every start, with its own settings.
- When they differ and
- Patch recipes (Plugin format, section 7).
- Each plugin's stamp in the world table
plugin_patchescovers the recipe text, the plugin version and the installer version. A plugin whose stamp changed is uninstalled and installed again with the same named ids. - Plugins no longer in
plugins/have theiruninstallSQL run. A disabled plugin (inplugins/.disabled/) keeps its named ids inplugin_ids, so enabling it again gives it the same ids; a removed plugin's ids are released, also when it was removed while disabled. - A plugin with a server library that did not load is left out.
- A failure is logged as
Plugin patches failed: …, and the server still starts.--applyand--deployreport it as failed.
- Each plugin's stamp in the world table
- Client archive. When the launcher knows the game folder, it passes it to the server (
LONELYICE_CLIENT). For every client locale, the server rebuildsData/<locale>/patch-<locale>-4.MPQwhen its stamp changed. The stamp covers the recipes, the plugin versions, the patch files, the named ids and the sizes of the source archives. LonelyIce's archive is removed when no recipe remains. An archive of that name that LonelyIce did not write (it has no LonelyIce marker inside) is never deleted or overwritten: before LonelyIce writes its own, it moves that one to the first free name ofpatch-<locale>-4.MPQ.bak,.bak2,.bak3, … and logs<locale>: patch-<locale>-4.MPQ was not written by LonelyIce, kept as <name>. If it cannot be moved, the patch step fails for the client and nothing is written. With no recipes it is left where it is.
Client addons (client.addons) are not handled at server start. The launcher copies them into Interface/AddOns
when it starts the game, and removes the addons of plugins that are gone.
--pkg apply -c <worldserver.conf> [--client <game folder>] runs steps 1 to 5 without starting the world
(Command line).
Icon cache
After the catalogs load, the launcher downloads the icon of every catalog entry to
plugins/.cache/icons/<id>-<version>.png. --pkg does not download icons.
| Rule | Effect |
|---|---|
| A file with that name exists | It is not downloaded again. |
| The download fails or is not a PNG | Nothing is saved. There is no error. |
| Old icons | Never deleted or refreshed. A new icon needs a new version. |
Installed plugins show icon.png from their own folder.
Failures and recovery
An install is all or nothing (see Installing).
| Failure | State afterwards |
|---|---|
| A catalog cannot be read | That catalog is skipped. |
| Resolution error (bad range, no suitable version, disabled dependency, conflict) | Nothing changed. |
| The server runs on the plugins folder | Nothing changed. |
| Download, size or sha256 check, unpacking or manifest check of any step | Nothing changed; plugins/.staging is deleted. |
| Moving an old folder aside or the staged folder in | Everything is moved back; nothing changed. |
| Moving back fails as well | The error lists the folders to move by hand; the old ones are in plugins/.backup/<time>. |
| The process dies while folders are moved | Some plugins may be new, the others old; the old folders of replaced ones stay in plugins/.backup/<time>. Install again. |
| Remove, enable or disable fails | The folder stays where it was, or partly deleted for a remove. |
After you fix the cause, run the same install or update again. On the Plugins page a failure sets the status to
failed and adds the error to the events. --pkg prints error: <message> and exits with code 1.