The installer of the bootstrapper 2.0
From 2.0 the starter installs an application for the Windows user the way Windows Installer installs a package per user: from a description of what it consists of, as one transaction that is rolled back on failure, with every shared part counted, so that update, going back, repair and uninstall leave exactly the state they promise. Nothing needs administrator rights.
When 2.0 installs and when 1.0 runs
| Delivery | Way |
|---|---|
| A project file of format 2 (any program or file; see The project file) | Installer of 2.0, general project |
The components folder of the delivery contains registration.json next to the DLL (release packages of WKL Forms Object Library from 1.57), or the delivery brings no components (components empty) | Installer of 2.0 (this page) |
Components delivered without registration.json (packages up to 1.56.1) | As 1.0 |
registration.json is the per-user registration of the DLL as data, written when the package is built
(tools\Export-Registration.ps1 of WKL Forms Object Library). The starter writes these keys itself instead of running
regsvr32; Windows Installer advises against self-registration because it cannot be rolled back safely and hides what it
writes.
What is installed, and where it is recorded
Each part of an installed version is a component with a key path; the component is installed when its key path exists. Every product version that uses a component is one of its clients - the reference count.
| Component (id) | What | Key path |
|---|---|---|
frontend/<product>/<version> | The copy of the application in %LOCALAPPDATA%\WKL\<product>\Frontends\<version>, the whole folder (also files Access or the application leave there), and the INI of ini | The database file |
components/<release>/<bitness> | The WKL components of one release and bitness in %LOCALAPPDATA%\WKL\Components\<release>\<bitness> and their registration below HKCU\Software\Classes (32-bit view for x86) | The DLL |
odbc/<driver>/<release>/<bitness> | An ODBC driver the delivery brings, in %LOCALAPPDATA%\WKL\Drivers\<driver>\<release>\<bitness>, and its data source for the user (see ODBC drivers) | The driver DLL |
trust/<product> | The trusted location WKL.<product> (see What the starter writes and reports) | The key |
apps-entry/<product> | The entry under Windows Settings > Apps: HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\WKL.<product> with DisplayName, DisplayVersion, Publisher, UninstallString ("<starter>" /uninstall), QuietUninstallString, DisplayIcon, InstallDate, NoModify, NoRepair | The key |
The record is the installer database of the user, %LOCALAPPDATA%\WKL\Installer\installer.db (SQLite, the copy of SQLite
that Windows ships; one database for all products of the user). Its tables:
| Table | Content |
|---|---|
Product | Product key, display name, source |
ProductVersion | Installed versions, State (installed/removed), LastWorking (the version started last) |
Component | Id, kind, release, bitness, key path |
ComponentClient | Which product version uses which component |
Resource | Every file, folder, registry key and value of a component, with Ownership: created (by the starter; removed with the last client) or found (was there before; never removed, put back to Previous) |
Txn, TxnStep | The journal: each operation, each step with its undo information |
How to check it from outside.
Wkl.Bootstrapper.exe /listprints every installed version and every component with its number of users; a component whose key path is missing is markedMISSING (repair: /repair)./list /jsonprints the same as JSON (see Commands and exit codes).- The database can be read with any SQLite tool, for example
SELECT * FROM ComponentClient. Read only: the starter is the only writer. - The log of the product (
Logs\bootstrapper.log) has one line per step: date and time with milliseconds, the process id, the step padded to 12 characters, then the text. Per install it writes a line with the stepinstall: for a delivery of format 1install <version> <copy>, followed bycomponents <release>when components were registered; for a project of format 2install <product> <version>: <n> entries, <n> registry keys. A delivery that uses an existing registration writescomponents using the existing registration ...instead.
Install and update
A start with a new version (or the first start) runs these steps as one transaction:
- An interrupted earlier transaction is rolled back first (the PC went off, the process was ended).
- On the first meeting with the product, what the starter 1.0 installed is taken over (see below).
- The version is recorded.
- The copy is placed - unless it exists: copied, checked against size and SHA-256 of
app-manifest.json(code 42 when it differs), the mark of the web removed; the INI written. - The components: an existing registration the starter did not make (the IT's for the computer,
HKLM, or the release package's own for the user) that points at an existing DLL of a release at least as high as the application requires (requiresofapp-manifest.json) is used as it is - registering again would double it, and COM would take the user's registration before a newer one of the computer. Otherwise the release rule decides; a release folder to be registered is copied (only missing files; a DLL in use is never replaced) and the registration written fromregistration.json. - The ODBC drivers of the delivery, each with its data source (see ODBC drivers).
- The trusted location and the entry under Apps are written or updated (
DisplayVersion). - The transaction is committed.
Afterwards, in a transaction of its own, versions beyond keepVersions are removed (never the version being started and
never the last working one), and then Access starts. A version started successfully becomes the last working one.
On failure every step done so far is undone in reverse order, the PC is as before the start, and the window reports the code:
| Code | Cause |
|---|---|
| 11 | registration.json is not a registration table |
| 32 | The delivered release is older than the application needs (and no sufficient registration exists) |
| 41 | A file could not be copied, moved or removed; when another program holds it, the message names it (for example a virus scanner) |
| 42 | The copy does not match the manifest |
| 50 | A policy forbids trusted locations of the user |
File steps wait and try again for a moment while another program (a virus scanner, the indexer) holds a file.
Example: the IT registered the components for the computer. WKL Forms Object Library 1.56 is registered below HKLM with
the DLL in C:\Program Files (x86)\WKL\Access Components, the application requires 1.51, and the delivery brings 1.56.1. The
starter registers nothing, copies no components and records no components/... component; the log has the line
components using the existing registration 1.56 for the computer: C:\Program Files (x86)\WKL\Access Components\Wkl.Access.Components.dll.
Were the registration for the computer 1.48, the delivered 1.56.1 would be registered for the user, and the user's
registration would win over the computer's from then on.
Which release is registered
One release of WKL Forms Object Library is registered per user and bitness at a time: the class keys are the same in every
release, so the last registration decides for every application of the user. Each product version records the release
its application requires (requires in app-manifest.json). The starter registers the delivered release unless that
would take a release away from another product:
| Delivered release | Registered release (by the starter) | Result |
|---|---|---|
| Lower than the application requires | Any | Code 32; nothing changes |
| At least what the application requires | None, or lower than the application requires | The delivered release is registered |
| Higher than the registered release | At least what the application requires | The delivered release is registered |
| The same as the registered release | The same | Nothing changes; the product version becomes one more client |
| Lower than the registered release | At least what the application requires | The delivered release is registered if no installed version of another product requires more than it; otherwise the registered release stays and the product version becomes one more client of it |
Only other products count. Versions of the same product kept beside the started one (keepVersions) do not: going back
within a product registers the release that version was delivered with, and going forward registers the newer one again.
Example: product A 1.4.0 requires 1.56 and was delivered with 1.56.1, A 1.5.0 requires 1.57 and was delivered with 1.57.0.
| Start | Registered afterwards |
|---|---|
| A 1.5.0 | 1.57.0 |
| A 1.4.0 (going back), no other product installed | 1.56.1 |
| A 1.5.0 again | 1.57.0 |
| A 1.4.0, while product B 1.0.0 requiring 1.57 is installed | 1.57.0 stays |
Two products with different releases therefore both run on the higher one: a running Access keeps the DLL it loaded when the registration changes, and Access processes started afterwards load the newly registered release.
The release of a delivery is the release of its registration.json, which Export-Registration.ps1 takes from the product
version of Wkl.Forms.ObjectLibrary.Managed.dll (up to release 1.68: Wkl.Access.Components.Managed.dll; for example
1.56.1). The version resource of the native start module (Wkl.Forms.ObjectLibrary.dll, up to 1.68
Wkl.Access.Components.dll) carries only major and minor (1.56.0 in 1.56.1) and is not used; the starter 1.0 used it, so
its folders below Components may carry that number.
ODBC drivers
A delivery may bring ODBC drivers for the user, so that no administrator has to install them. Each driver lies in the
delivery as odbc\<driver>\x86 and odbc\<driver>\x64 and is named in bootstrapper.json:
{
"format": 1,
"product": "Customers",
"source": "\\\\server\\apps\\kunden\\production",
"components": "components",
"odbcDataSources": [ "WklGateway" ],
"odbc": [
{ "driver": "Wkl OData", "folder": "odbc\\Wkl OData", "dll": "Wkl.Data.OData.Odbc.dll", "dataSource": "WklGateway" }
]
}
| Field | Meaning |
|---|---|
driver | The driver's name; also the folder below %LOCALAPPDATA%\WKL\Drivers and the value in ODBC Data Sources |
folder | The folder of the driver in the delivery, relative to source; the starter takes the subfolder of the bitness of Access |
dll | The driver DLL in that subfolder |
dataSource | The user data source the application connects through |
The starter copies the subfolder to %LOCALAPPDATA%\WKL\Drivers\<driver>\<release>\<bitness>, where <release> is the product
version of the DLL without a build id, and writes the data source for the user (32-bit view of the registry for x86):
| Key | Value | Data |
|---|---|---|
HKCU\Software\ODBC\ODBC.INI\<dataSource> | Driver | The full path of the copied DLL |
Description | <driver> (Wkl.Bootstrapper) |
|
HKCU\Software\ODBC\ODBC.INI\ODBC Data Sources | <dataSource> | <driver> |
No ODBCINST.INI entry is written; Access opens linked tables and pass-through queries through a data source
whose Driver is the path of the DLL. Everything else - the service address, the database - belongs into the connection
string of the application's links, not into the data source. A data source named in odbc is not required to exist before
the start, unlike the other entries of odbcDataSources.
The driver is a counted component like the WKL components: a newer release delivered later is placed beside the old one and the data source points to it; an uninstall removes the release folder and the data source with the last product version using them. A data source of that name that existed before the starter wrote it is put back to its previous values.
Unlike the WKL components, the data source follows no release rule: every start writes it for the version started. Going
back to a version delivered with an older driver release points the data source at that older release again. When two
products deliver the same dataSource with different driver releases, the product started last decides; no code is
reported, and both release folders stay as long as a version uses them. Give each product its own dataSource when they
must not share a driver release.
Going back
Starting an older version - a source that delivers a lower version again - installs that version again: its copy (if it is
still there), its drivers (the data source points at the older driver release again) and its components release under the
release rule. Newer versions
stay until keepVersions or the uninstall removes them, so the user can go forward again.
With deliveryKey in the project file, a source delivers a lower version only when the publisher withdrew the higher one in
the signed delivery (/publish /withdraw); otherwise the starter keeps the installed version and reports code 52. See
How to: Publish a signed delivery.
Going back while the source already delivers a newer version works from the settings window
(Wkl.Bootstrapper.exe /settings). Its three actions run on the installer, each as one transaction with its rollback:
| Action | What it does |
|---|---|
| Start this version | Registers again what the chosen version ran on: its WKL components release (from the installed release folder, under the release rule) and its data sources. Then it starts Access with that version's copy and marks it as the last working version, which the cleanup keeps. Nothing is copied from the source. |
| Copy again | Removes the chosen version with every component only it used. The next start installs the version the source delivers again. Refused at once while Access has the copy open. |
| Remove old versions | Removes the versions beyond keepVersions, as a start does. It never removes the version the source delivers or the last working version. |
Start this version brings back the components release the version used, not the one its delivery contained. A version that stayed on a newer release because another product needed it stays on that newer release.
A product that only the bootstrapper 1.0 placed keeps the 1.0 actions on its folders until its next start takes it over.
Repair
Wkl.Bootstrapper.exe /repair runs the install of the current version without starting Access: whatever is missing
is put back from the delivery - the copy if deleted, files of the components, registry values that were deleted or changed,
the trusted location, the entry under Apps. What is in place is not touched; an existing copy keeps the user's data.
Uninstall
Wkl.Bootstrapper.exe /uninstall (also the command Windows runs from Settings > Apps) removes, as one transaction:
- every version of the product: the version folders with everything in them;
- every component no other product version of the user still uses: files, folders, and registry keys and values the starter created;
- registry values that existed before the starter wrote them are put back to their previous content, never deleted;
- the trusted location and the entry under Apps.
Components another product of the user still uses stay, and with them the class keys they share; when the removed product used the registered release, the newest remaining release of the same bitness is registered again. When a copy is open in Access, nothing is removed and the code is 41 with the program named; close Access and run it again.
The shortcuts of the project's shortcuts list are removed with the product, also those whose target is the starter itself.
Not touched: the folder the starter runs from with its bootstrapper.json, shortcuts that do not come from the project, the
installer database itself and the logs below %LOCALAPPDATA%\WKL\<product>\Logs.
Commands and exit codes
| Command | Does | Exit code |
|---|---|---|
| (none) | Installs or updates, then starts Access | The code of the run; 0 when Access started |
/quiet | The same without any window | As above |
/repair | Installs what is missing, does not start Access | The code of the run; 0 when complete |
/uninstall [/quiet] | Removes the product of the bootstrapper.json next to the EXE. Without /quiet (the entry under Windows “Apps”) the starter first asks in its window, in the language and color scheme of the project; Cancel is the default button, so Enter, Esc and closing the window keep everything. With /quiet (scripts, QuietUninstallString) it removes without a question | 0 removed; 1602 canceled, nothing removed (the code of msiexec); 10 bootstrapper.json missing; 41 in use or failed (rolled back); 43 a beforeUninstall action failed (nothing removed) |
/list | Prints the installed versions and components | 0 |
/list /json | The same as JSON | 0 |
/RUNCFG <file> with any command | Uses <file> (a .json file in the starter's folder, a plain file name) instead of bootstrapper.json: several applications beside one starter. The entry under Apps uninstalls with the same switch. | As the command; 10 for a name with a path |
/new-key <name> | Creates the publisher's delivery key in the Windows key store of the user and prints its public part for deliveryKey | 0; 10 the name exists |
/publish [/key <name>] [/withdraw <version>[,<version>]] | Writes delivery.json (and with a key delivery.json.sig) into the source of the project file; /withdraw names the higher versions this delivery replaces; see How to: Publish a signed delivery | 0; 11 a withdrawn version is no higher version; 12 source not reachable; 51 key missing or not the project's |
/whatif <file> [/json] | A dry run of the project file: every check of a start and the plan of what it would install where, without changing anything; see What the starter writes and reports | As /diagnose: 0 when a start would succeed, else the error code |
/check <file> [/json] | Checks a project file against its rules and prints every problem; with /json an object with valid, format and problems. Reads only the project file: whether the delivery holds what it names (for example registration.json of the components) is checked by /whatif. From 2.3.0 a file ending in .msi.json is checked as installer settings, including the files they name | 0 valid; 10 file not found; 11 problems |
/msi-sources <project file> /out <folder> [/starter <exe>] [/version <x.y.z>] | Version 2.3.0 and later: writes the WiX sources of the starter's package from <name>.msi.json beside the project file (or of the package without dialogs when there is none) and prints, as JSON, the packages to build (builds with scope, culture, output, sources, localization; images to convert from PNG). Used by tools\New-StarterMsi.ps1; needs no WiX Toolset | 0 written; 10 project file, folder or starter missing; 11 a rule broken or a page that does not fit |
/import-reg <file.reg> [/json] | Reads a .reg file of Registry Editor into registry entries of a project (only below HKEY_CURRENT_USER; HKEY_CLASSES_ROOT becomes Software\Classes of the user, WOW6432Node the 32-bit view) and lists what was left out (keys of the computer, binary values, deletions). Writes nothing; with /json the project's registry list and notes, ASCII only | 0 read; 10 file not found; 11 no .reg file |
/new-project <file> /template <names> [/json] | Writes a new project file of format 2 from templates; <names> is a comma-separated list of access (the application of the build tool's app-manifest.json, started as it says, with the condition Microsoft Access), components (WKL Forms Object Library from components\x86 and components\x64 of the delivery folder, registered for the user, with the condition .NET 10 Desktop Runtime), odbc (an example ODBC driver with a data source of the user per bitness, to be replaced with the delivered one) and json (WKL Office JSON from json\x86 and json\x64) and dotnet (a private .NET runtime from dotnet\x86 and dotnet\x64, kept per release below [WklRoot]\Dotnet and handed to the started program as DOTNET_ROOT(x86) or DOTNET_ROOT; it replaces the condition .NET 10 Desktop Runtime of components, but only for programs the starter starts); none gives a project that starts nothing yet. The product key is the name of the file's folder, the delivery folder deploy beside the starter. Lists what is left to do; with /json an object with written, notes and problems, ASCII only | 0 written; 10 no file named; 11 unknown template, or the file exists |
/import-starterpro <xml> [/out <file>] [/json] | Converts a configuration of the former starter into a project file of format 2 (by default bootstrapper.json beside it) and lists what was not taken over; with /json an object with written, notes and problems, every character outside ASCII escaped; see How to: Move from the former starter | 0 written; 10 configuration not found; 11 no configuration of the former starter, or the project file exists |
The codes are those of Error codes of the starter. /list /json prints one object:
{
"versions": [
{ "product": "Customers", "displayName": "Customers", "version": "1.4.0", "lastWorking": true }
],
"components": [
{ "id": "components/1.56.1/x86", "keyPath": "C:\\Users\\<user>\\AppData\\Local\\WKL\\Components\\1.56.1\\x86\\Wkl.Access.Components.dll", "clients": 2, "present": true }
]
}
| Field | Meaning |
|---|---|
versions[].product | Product key of bootstrapper.json |
versions[].displayName | Name shown under Apps |
versions[].version | Version of the application (app-manifest.json) |
versions[].lastWorking | True for the version started successfully last |
components[].id | Component id, see What is installed |
components[].keyPath | File or key whose presence means installed |
components[].clients | Number of product versions using the component |
components[].present | False when the key path is missing; /repair puts it back |
With nothing installed the arrays are empty.
Testing that a failed install leaves nothing
To see a rollback on a test PC, let the copy fail after the version was recorded:
- Run
/list /jsonand keep the output. - Place a new version in the source and change one byte of its database file after
app-manifest.jsonwas written. - Start the starter. It reports code 42.
- Run
/list /jsonagain: the output equals step 1, and no folder of the new version exists belowFrontends.
To see that the registration of the components and the data source are taken back as well, let a step after them
fail. The driver is placed after the components (step 6 after step 5), so a delivery with an odbc entry serves:
- Run
/list /jsonand note the values of the components' class key (for exampleHKCU\Software\Classes\WklForms.Application\CLSID, up to release 1.68WklComponents.Application, and itsInprocServer32) and ofHKCU\Software\ODBC\ODBC.INI\<dataSource>. - Place a new version in the source whose delivery brings a newer components release and the driver.
- Hold the delivered driver DLL open so that it cannot be read, for example in PowerShell:
$f = [System.IO.File]::Open('<source>\odbc\<driver>\x86\<dll>', 'Open', 'Read', 'None'). - Start the starter. After a few seconds of retrying it reports code 41.
- Compare:
/list /json, the class key and the data source equal step 1; no folder of the new release exists below%LOCALAPPDATA%\WKL\Componentsor%LOCALAPPDATA%\WKL\Drivers. - Release the file (
$f.Dispose()) and start again: the new version installs.
A policy against trusted locations does not serve this purpose: the starter checks it before anything is installed (code 50
without a transaction), and the policy keys below HKCU\Software\Policies cannot be written without administrator rights.
To see that an uninstall does not remove half of a product, open the installed copy in Access and run /uninstall /quiet: code 41
names MSACCESS.EXE, and /list still shows every version and component.
Taking over the starter 1.0
The first time 2.0 meets a product, it records as its own what the starter 1.0 left: the version folders below Frontends
(the last working version from State\provisioning.json), the trusted location 1.0 wrote (its description ends with
(Wkl.Bootstrapper)), and the registration 1.0 made with regsvr32 from %LOCALAPPDATA%\WKL\Components. They are
then cleaned up and uninstalled like parts placed by 2.0. Registrations from anywhere else are not taken over.
Limits of the installer
- Two starters of the same Windows user installing at the same moment are not serialized: the installer database is one per user, and a starter that finds an unfinished installation rolls it back, also one that another starter is running. Start one at a time (format 1 without
registration.jsonserializes per product). - A file of format 1 does not deliver the .NET Desktop Runtime: the starter checks it (code 33). A project of format 2 can deliver a private runtime; see How to: Deliver a private .NET runtime.
- WKL JSON is not delivered by a format 1 file: an application that uses it needs it registered beforehand, for example by the IT. A project of format 2 delivers it as a
registerComentry like any COM server. - Computers that restrict programs in the user's profile with AppLocker or Windows Defender Application Control are not supported.
- A source over HTTP/HTTPS is not supported; the source is a folder or a share.