taler-windows

Windows packaging
Log | Files | Refs

commit 2d7a43ded79da78587e0e874de66e474d6bc72e7
parent 9f9d57832f92792bef406d14bb6089ae3641489f
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun,  1 Nov 2026 02:10:48 +0100

- document the installer, its package contract and the test harness
- stop the WSL bootstrap from blocking on hidden prompts
- install the distro and its user without any prompt
- install the WSL runtime from Microsoft's MSI when it is missing
- only report success when the install actually succeeded

Assisted by LLM

Diffstat:
ACLAUDE.md | 262+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aget-wsl-msi.ps1 | 48++++++++++++++++++++++++++++++++++++++++++++++++
Minstall-taler-from-apt.sh | 41+++++++++++++++++++++++++++++------------
Mset-db.sh | 31++++++++++++++++++++++---------
Msetup-wsl-debian.bat | 154++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mtaler-installer-taler.nsi | 57++++++++++++++++++++++++++++++++++++++++++---------------
Mtest-db.sh | 1+
7 files changed, 525 insertions(+), 69 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md @@ -0,0 +1,262 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A Windows **installer** for the GNU Taler merchant backend. There is no Taler source code +here — the backend is installed from the `deb.taler.net` APT repository *inside WSL*. This +repo is only the orchestration layer: an NSIS wizard that collects user choices, a batch +script that drives `wsl.exe`, and three bash scripts that run inside the WSL distro. + +Twelve tracked files, no build system, no test suite. It is also the odd one out in the +`/home/claude` workspace: nothing here links against `~/target`, and the build-order and +`make install` rules in the parent `CLAUDE.md` do not apply. + +## Building + +```sh +makensis taler-installer-taler.nsi # produces taler-installer.exe (gitignored) +``` + +`makensis` is **not installed in this workspace** and the result only runs on Windows with +WSL available, so changes here cannot be compiled or exercised locally — they are reviewed +by reading. Required at compile time in the same directory: `logo-header.ico`, +`logo-header.bmp`, `logo-welcome.bmp`, and the four scripts listed in the `File` directives. + +## Control flow + +``` +taler-installer.exe (NSIS, RequestExecutionLevel admin) + wizard pages: Welcome → Directory → Distro → Domain → ReverseProxy → INSTFILES + Section "Install Taler-Merchant" installs the 4 scripts to $INSTDIR, then: + setup-wsl-debian.bat 1 "" <distro> → steps 1 only + setup-wsl-debian.bat ALL "" <distro> → steps 1..8 + setup-wsl-debian.bat 8 <domain> <distro> <proxy_mode> → step 8 only + post-install pages: AskExternalDB → DBUrl → calls test-db.sh / set-db.sh in WSL + .onGUIEnd: restarts apache2, opens http(s)://<domain> in the browser +``` + +`setup-wsl-debian.bat` is a **step dispatcher**: `%1` is a step number (`1`-`8`) or `ALL`. +It `goto`s the matching `:stepN` label and each step ends with +`if not "%STEP%"=="ALL" exit /b`, so a numeric argument runs one step and `ALL` falls +through all of them. Arguments are positional: `STEP DOMAIN DISTRO PROXY_MODE`. + +The steps: (1) enable `Microsoft-Windows-Subsystem-Linux` + `VirtualMachinePlatform`, +`wsl --update`; (2) `wsl --install -d <distro>`; (3) create the `taleruser` account, +grant it `NOPASSWD` sudo, make it the default in `/etc/wsl.conf`, terminate the distro to +apply; (4) add it to `www-data`; (5) copy the three `.sh` files through `%TEMP%` into +`/home/taleruser/taler-installer` via `type | wsl tee`, `dos2unix`, `chmod +x`; +(6) verify format/permissions; (7) `wsl --shutdown`, then run +`install-taler-from-apt.sh`; (8) install Apache and run `taler-merchant-rproxy-setup`. + +`install-taler-from-apt.sh` is likewise **multi-mode on `$1`**: `8 <domain>` does the +HTTPS reverse proxy only, `http <domain>` the HTTP-only proxy only, and no recognised first +argument means the full install (postgres, Taler APT repo keyring for the detected +Debian/Ubuntu codename, `taler-merchant`, certbot, `taler-merchant-dbconfig`, enable the +systemd units). Both proxy branches are currently dead from the batch path, because step 8 +calls `taler-merchant-rproxy-setup` itself. + +Fixed identities the scripts assume everywhere: WSL user `taleruser`, Taler DB role +`taler-merchant-httpd`, scripts at `/home/taleruser/taler-installer/`. + +## The contract with the taler-merchant package + +Most of this installer's history of breakage came from fighting the Debian package +instead of using it, so know what `taler-merchant` already provides: + +- **`/etc/taler-merchant/taler-merchant.conf` is an entry point, not a settings file.** + It carries `[paths]` and three include directives: `@inline-matching@ conf.d/*.conf` + (which is where `[merchant] SERVE = systemd` lives), `@inline@ overrides.conf`, and + `@inline-secret@ merchantdb-postgres ../secrets/merchant-db.secret.conf`. + **Put overrides in a `conf.d/` drop-in**, never in this file. + Overwriting it is *not* immediately fatal — measured, not assumed: the defaults under + `/usr/share/taler-merchant/config.d/` still supply `SERVE = tcp` and + `CONFIG = postgres:///taler-merchant`, and systemd socket activation overrides + `SERVE = tcp` anyway, so the backend still answers. What is lost is everything an + administrator puts under `/etc/taler-merchant`: every `conf.d/` drop-in, `overrides.conf`, + and the database secret are silently no longer included. +- **`SERVE` must be `tcp`, `unix` or `systemd`** (`exchange/src/mhd/mhd_config.c:194`); + if it resolves nowhere at all, `taler-merchant-httpd` aborts with *"serve type (tcp, + unix or systemd) required"*. When `LISTEN_FDS` is set, systemd activation wins over + `SERVE` and only logs a warning. There is no `UNIX_DOMAIN_SOCKETS` option anywhere in + the Taler codebase. +- **The backend is socket-activated.** `taler-merchant-httpd.socket` listens on + `/run/taler-merchant/httpd/merchant-http.sock` (`taler-merchant-httpd:www-data`, mode + 0660) and the service has `Requires=` it. Enable the *socket* (or the target), not the + service. There is no need for a TCP port such as 8888. +- **The package ships the Apache vhost** at `/etc/apache2/sites-available/taler-merchant.conf`, + already proxying to that unix socket, with `%%your.domain%%` placeholders that + `taler-merchant-rproxy-setup` substitutes. +- **The database URI** goes in `secrets/merchant-db.secret.conf` as + `[merchantdb-postgres] CONFIG=postgres://...`. + +`taler-merchant-rproxy-setup` (in the `merchant` checkout) accepts **only** `--domain`, +`--nginx|--apache`, `--httponly|--httpsonly` and `-h`. Any other argument is a hard +`exit 1`. It runs `certbot certonly --webroot -w /var/www/html`, so plain `certbot` is +the only certbot package needed — **never** `python3-certbot-nginx`, which `Depends: nginx` +and leaves nginx holding port 80 so the apache2 installed in step 8 cannot start. + +## Logs + +Scattered, and worth knowing before debugging a user report: + +| Path | Written by | +| --- | --- | +| `C:\Users\Public\taler-bat.log` | `setup-wsl-debian.bat` (truncated at each start) | +| `C:\Users\Public\taler-sh.log` | `install-taler-from-apt.sh` (via `exec > >(tee -a)`) | +| `C:\Users\Public\install-bat.log` | copied out of WSL at the end of step 7 | +| `/home/taleruser/taler-installer/install-sh.log`, `test-db.log` | the bash scripts | +| `/tmp/dbtest.log` | `test-db.sh` | + +`readme.txt` and `.onInstFailed` both point at `C:\Users\Public\install.log`, which nothing +writes. + +## Test harness + +There is a working harness on `tripwire.taler.net` under `/srv/winstall-test` +(not in this repo yet). It is the only way these scripts get exercised, since nothing +here can run on the development workstation. + +``` +harness/run-debian-test.sh <fresh|twice|reinstall> <ssh-port> <repo-dir> <domain> [label] +``` + +It boots a throwaway Debian 13 cloud image under qemu (user-mode networking, qcow2 +overlay destroyed afterwards), creates `taleruser` the way the batch file would, runs +`install-taler-from-apt.sh` plus `taler-merchant-rproxy-setup`, and asserts the result — +ending with a real `GET /config` both over the backend's unix socket and through Apache. +`twice` runs the installer a second time on a dirty system; `reinstall` purges +`taler-merchant` in between. Point `<repo-dir>` at a checkout of `master` or of a fix +branch to get a before/after. + +Two traps cost time when extending it: `ssh` inside the driver needs `-n` or it eats the +script's stdin and hangs forever, and assertion patterns must use `grep -F` — `[paths]` +as a regex is a character class that matches almost any line. + +## Verified end to end on Windows + +Three scenarios, each from a fresh overlay of a clean Windows 11 Enterprise +Evaluation (build 26300) with the WSL features *disabled* and no WSL runtime -- +i.e. a real first install. All three pass 14/14, ending with +`http://localhost/config` answered **from Windows**, which is what a merchant's +browser hits: + +| scenario | what it does | result | +| --- | --- | --- | +| `fresh` | enable features, reboot, MSI bootstrap, distro + user, Taler, HTTP proxy | 14/0 | +| `twice` | the above, then the whole installer again over existing state | 14/0 | +| `reinstall` | the above, then `wsl --unregister` and install again | 14/0 | + +Run them with `harness/win-test.sh run <fresh|twice|reinstall>`; they use +separate ports, overlays and firmware vars, so all three run in parallel. + +Two traps when editing the batch file, both of which silently aborted a run: +`cmd` parses `%~` substitutions **even inside `::` comments**, and `start`, +`timeout` and `pause` all refuse to run with redirected stdin. + +## What the Windows VM proved + +Measured on a clean Windows 11 Enterprise Evaluation (build 26300, 26H2) under +qemu/KVM, not inferred: + +- **Step 1 is fine.** Both optional features go `Disabled` -> `Enabled`, the + reboot gate works, and the machine comes back. +- **WSL2 itself runs** in that nested setup: `vmmemWSL` live, `wsl -l -v` + reporting `Debian / Running / VERSION 2`. WSL prints *"nested virtualization is + not supported on this machine"*, which concerns nesting *inside* WSL and is not + fatal. +- **`cmd`'s `start` and `timeout` refuse redirected stdin**, printing + *"Input redirection is not supported, exiting the process immediately"*. nsExec + runs this batch file exactly that way, so `wsl --update` never executed and all + five `timeout` delays were silent no-ops. Use `ping -n N 127.0.0.1 >nul` for a + delay in this file, never `timeout`. +- **`wsl --install -d <distro>` blocks on an invisible prompt** -- + *"Please create a default UNIX user account"* -- observed hung for 54 minutes. + `--no-launch` is required; step 3 creates `taleruser` itself anyway. +- **The in-box `wsl.exe` is only a stub.** Before the WSL runtime is present it + answers *"The Windows Subsystem for Linux is not installed"* to every form + except a bare `--install`, prints no `--help` at all, and cannot reliably + bootstrap itself on an image without the Store. Installing the official MSI + (`wsl.<ver>.x64.msi` from `microsoft/WSL` releases) with + `msiexec /i wsl.msi /quiet /norestart` works silently and returns 0 — that is + the deterministic way to get WSL onto a merchant machine. +- **`wsl --install` cannot create the UNIX account.** Its complete option set is + `--enable-wsl1 --fixed-vhd --from-file --legacy --location --name + --no-distribution --no-launch --version --vhd-size --web-download`. The + account is only ever created by the distro's interactive first-run. The + supported non-interactive sequence, verified end to end in the VM, is: + `wsl --install -d <Distro> --no-launch --web-download`, then + `wsl -d <Distro> -u root -- useradd -m <user>`, then + `wsl --manage <Distro> --set-default-user <user>`. +- **systemd is already PID 1 in the Debian WSL image.** It ships `/etc/wsl.conf` + containing `[boot] systemd=true`, `ps -p 1 -o comm=` returns `systemd` and + `systemctl is-system-running` returns `running`. The `systemctl` calls these + scripts make are therefore fine — but hand-appending `[user]` to that file + risks clobbering the `[boot]` stanza, which is why `--set-default-user` is + preferred. +- **The installer always claims success.** The final + *"GNU Taler-Merchant installed successfully"* line prints unconditionally, two + seconds after the log records `return code 1` from the reverse-proxy step. + +## Known rough edges + +Fixed on the current review branch (see `git log`): the non-existent +`--acme`/`--merchant-url` arguments, the `%errorlevel%` expansion bug that hid every +reverse-proxy failure, the `taler-merchant.conf` overwrite, the `python3-certbot-nginx` +port-80 conflict, enabling the service instead of the socket, `set-db.sh` writing a +`DATABASE_URL` key that nothing reads, the mid-step `exit /b` that made `STEP=3` do only +half of step 3, and `$DOMAIN_INPUT` being used both as an nsDialogs control handle and as +the domain text (which broke the "localhost implies HTTP-only" guard). + +Still open, and worth knowing before debugging a user report: + +- **`rproxy-setup` requires the domain to resolve to a local IP.** It runs + `dig +short A "$DOMAIN"` and compares against `hostname -I`, exiting 1 on a mismatch. + Inside WSL the interface is NAT'd (172.x), so a real public domain never matches, and + `localhost` has no A record at all. The check is skipped entirely when `dig` is absent + — `bind9-dnsutils` is priority-standard but not a dependency of `taler-merchant`, + `certbot`, `apache2` or `postgresql`, so whether this fires depends on the WSL image. + Fixing it properly means relaxing the check to a warning in the `merchant` checkout + (its own comment suggests exactly that), which is a separate repo and review. +- **`certbot certonly` is interactive** (email, TOS) and `rproxy-setup` passes no + `--non-interactive --agree-tos -m`, so HTTPS mode can block on a prompt that nobody + sees behind `nsExec`. Also a `merchant`-side change. +- **No silent/unattended mode.** All four inputs come from `nsDialogs` pages, which `/S` + skips, leaving the variables empty; the ~8 `MessageBox` calls block regardless of `/S` + (NSIS does not suppress them). `FileFunc.nsh` is already included, so `GetOptions` for + `/DOMAIN=` etc. is close at hand. +- **Step 1 demands a reboot and a full re-run**, persisting no state, so the wizard + re-asks everything. For automated runs, snapshot a VM with the WSL features already + enabled. +- **`.onGUIEnd` shows a MessageBox and opens a browser on any exit**, including user + cancel and failure. +- **Progress percentages are fake** — `Sleep`-driven `DetailPrint` lines, because the + long `nsExec` calls block the GUI. +- **Start-menu shortcut** points at `$INSTDIR\taler-installer.exe`, which the section + never installs. +- **`%DATE%`/`%TIME%` inside parenthesised blocks** all expand at parse time, so every + log line in one block shares a timestamp. +- Timestamps aside, `MessageBox`/`DetailPrint` text and the log messages are a mix of + English and French. + +## Conventions + +- Shell scripts are committed **LF**; `.bat` and `.nsi` are **CRLF** (NSIS and `cmd.exe` + want it). Step 5 runs `dos2unix` in WSL as a belt-and-braces measure, so a stray CRLF in + a `.sh` survives — keep them LF anyway. +- GPLv2.1+ header with `Author: Léa Oualli` on the `.bat`/`.nsi`/`.sh` files that have one. +- Comments and some log messages are in **French**, user-facing strings are bilingual: every + new wizard string needs a `LangString` for both `${LANG_ENGLISH}` and `${LANG_FRENCH}`. + Avoid accented characters in `LangString` values — the existing French ones are written + unaccented ("Etape", "redemarrer") because of NSIS codepage handling. +- `test-if.bat` is a scratch file for checking the `MODE`/`TYPE` branch logic, not part of + the installer. `run-admin.bat` just re-launches `setup-wsl-debian.bat` elevated via + PowerShell for manual testing outside the wizard. + +## Git + +Same workspace rule as the sibling checkouts: `origin` is a read-only mirror; work goes on +`dev/phoenix-bot/<topic>` pushed to the `durian` remote, via the **`taler-push`** skill. +This repo's history, unlike the other checkouts', is a handful of informal French commit +messages — follow the skill's format for new commits rather than that precedent. diff --git a/get-wsl-msi.ps1 b/get-wsl-msi.ps1 @@ -0,0 +1,48 @@ +# Downloads Microsoft's official Windows Subsystem for Linux installer. +# +# This file is part of GNU Taler. +# Copyright (C) 2026 Taler Systems SA +# +# The in-box wsl.exe in System32 is only a stub: on a machine with no WSL +# runtime it answers "The Windows Subsystem for Linux is not installed" to +# --update, to a bare --install and to every flagged form, so it cannot be +# relied on to bootstrap itself. The MSI installs silently with msiexec. +# +# Usage: get-wsl-msi.ps1 <output-path> +param([Parameter(Mandatory=$true)][string]$OutFile) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +try { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +} catch { } + +try { + $release = Invoke-RestMethod -UseBasicParsing -TimeoutSec 120 ` + -Uri 'https://api.github.com/repos/microsoft/WSL/releases/latest' ` + -Headers @{ 'User-Agent' = 'taler-windows-installer' } + + $asset = $null + foreach ($a in $release.assets) { + if ($a.name -like 'wsl.*.x64.msi') { $asset = $a; break } + } + if ($null -eq $asset) { + Write-Output "No x64 MSI in WSL release $($release.tag_name)" + exit 2 + } + + Write-Output "Downloading $($asset.name) ($([math]::Round($asset.size/1MB)) MB)" + Invoke-WebRequest -UseBasicParsing -TimeoutSec 1800 ` + -Uri $asset.browser_download_url -OutFile $OutFile + + if (-not (Test-Path $OutFile)) { exit 3 } + if ((Get-Item $OutFile).Length -lt 1MB) { exit 4 } + + Write-Output "Saved to $OutFile" + exit 0 +} +catch { + Write-Output "WSL MSI download failed: $($_.Exception.Message)" + exit 1 +} diff --git a/install-taler-from-apt.sh b/install-taler-from-apt.sh @@ -23,7 +23,7 @@ TALER_DB_USER="taler-merchant-httpd" if [ "$1" = "8" ] && [ -n "$2" ]; then DOMAIN="$2" echo "Configuring HTTPS reverse proxy for domain: $DOMAIN" - if ! sudo taler-merchant-rproxy-setup --acme --merchant-url="https://$DOMAIN:8888/"; then + if ! sudo taler-merchant-rproxy-setup --domain "$DOMAIN" --apache --httpsonly; then echo "WARNING: Impossible d’obtenir un certificat HTTPS valide pour $DOMAIN (vérifie le DNS, les ports 80/443, et Internet)" | tee -a "/home/$WSL_USER/taler-installer/install-sh.log" # On ne bloque pas toute l'install, on log juste le problème exit 0 @@ -107,16 +107,34 @@ sudo apt-get install -y taler-merchant || { exit 1 } -sudo apt-get install -y certbot python3-certbot-nginx || { +# Only the "certbot" package is required: taler-merchant-rproxy-setup runs +# "certbot certonly --webroot", which is built into certbot core. Do NOT +# add python3-certbot-nginx here -- it Depends on nginx, and an nginx +# holding port 80 prevents the apache2 installed in step 8 from starting, +# which in turn makes taler-merchant-rproxy-setup fail. +sudo apt-get install -y certbot || { echo "Certbot installation failed." >&2 exit 1 } -echo "Creating /etc/taler/merchant.conf..." -sudo mkdir -p /etc/taler -echo "[merchant] -PORT = 8888 -UNIX_DOMAIN_SOCKETS = NO" | sudo tee /etc/taler-merchant/taler-merchant.conf +# Do NOT overwrite /etc/taler-merchant/taler-merchant.conf. The package +# ships it as the entry point that pulls in everything else: +# [paths] -> TALER_RUNTIME_DIR, TALER_CONFIG_HOME, ... +# @inline-matching@ conf.d/* -> [merchant] SERVE = systemd +# @inline-secret@ ... -> [merchantdb-postgres] CONFIG (database) +# Replacing it removed the mandatory SERVE option, so taler-merchant-httpd +# aborted with "serve type (tcp, unix or systemd) required", and removed the +# database configuration. It also set UNIX_DOMAIN_SOCKETS, which is not an +# option the merchant reads at all. The shipped defaults are what we want: +# socket activation on /run/taler-merchant/httpd/merchant-http.sock, which +# the Apache vhost shipped by the same package proxies to. Any override +# belongs in a conf.d/ drop-in, never in this file. +MAIN_CONF="/etc/taler-merchant/taler-merchant.conf" +if [ ! -f "$MAIN_CONF" ]; then + echo "Expected $MAIN_CONF to be shipped by the taler-merchant package." >&2 + exit 1 +fi +echo "Using packaged configuration at $MAIN_CONF" echo "Initializing database with taler-merchant-dbconfig..." if ! sudo taler-merchant-dbconfig; then @@ -124,14 +142,13 @@ if ! sudo taler-merchant-dbconfig; then exit 1 fi -echo "Enabling taler-merchant.target..." +# taler-merchant-httpd.service has Requires=taler-merchant-httpd.socket and +# is started through socket activation: enable the socket, not the service. +echo "Enabling taler-merchant-httpd.socket and taler-merchant.target..." +sudo systemctl enable taler-merchant-httpd.socket sudo systemctl enable taler-merchant.target sudo systemctl restart taler-merchant.target -echo "Also enabling TCP service on port 8888..." -sudo systemctl enable taler-merchant-httpd.service -sudo systemctl restart taler-merchant-httpd.service - echo "Checking taler-merchant status..." sudo systemctl status taler-merchant-httpd.service --no-pager || true sudo systemctl status taler-merchant-httpd.socket --no-pager || true diff --git a/set-db.sh b/set-db.sh @@ -2,24 +2,37 @@ # set-db.sh # Usage: ./set-db.sh <POSTGRES_URL> POSTGRES_URL="$1" -CONF_PATH="/etc/taler-merchant/taler-merchant.conf" + +# The database URI belongs in the secret snippet that the packaged +# taler-merchant.conf pulls in with +# @inline-secret@ merchantdb-postgres ../secrets/merchant-db.secret.conf +# as [merchantdb-postgres] CONFIG. "DATABASE_URL" is not an option the +# merchant reads, and appending it to taler-merchant.conf landed it under +# whatever section happened to come last, so it never had any effect. +SECRET_PATH="/etc/taler-merchant/secrets/merchant-db.secret.conf" if [ -z "$POSTGRES_URL" ]; then echo "Missing PostgreSQL URL argument." exit 1 fi -# Remplace ou ajoute la ligne DATABASE_URL -if grep -q "^DATABASE_URL" "$CONF_PATH"; then - sudo sed -i "s|^DATABASE_URL *=.*|DATABASE_URL = $POSTGRES_URL|" "$CONF_PATH" -else - echo "DATABASE_URL = $POSTGRES_URL" | sudo tee -a "$CONF_PATH" +if [ ! -f "$SECRET_PATH" ]; then + echo "Expected $SECRET_PATH to be shipped by the taler-merchant package." >&2 + exit 1 fi -echo "Updated DATABASE_URL in $CONF_PATH" +# Rewrite in place via tee so the file keeps its owner and permissions. +printf '%s\n' "[merchantdb-postgres]" "CONFIG=$POSTGRES_URL" \ + | sudo tee "$SECRET_PATH" > /dev/null + +echo "Updated [merchantdb-postgres] CONFIG in $SECRET_PATH" + +# The external database still needs the schema before the backend can use it. +if ! sudo taler-merchant-dbconfig; then + echo "taler-merchant-dbconfig failed for the external database." >&2 + exit 1 +fi -# Redémarre les services pour prendre en compte la modif sudo systemctl restart taler-merchant.target -sudo systemctl restart taler-merchant-httpd.service exit 0 diff --git a/setup-wsl-debian.bat b/setup-wsl-debian.bat @@ -20,16 +20,22 @@ setlocal enabledelayedexpansion REM -------------------------- LOGFILE GLOBALE --------------------------- set LOGFILE=C:\Users\Public\taler-bat.log -echo [%DATE% %TIME%] === [START] GNU Taler-Merchant Installer === > %LOGFILE% +echo [%DATE% %TIME%] === [START] GNU Taler-Merchant Installer === >> %LOGFILE% echo [%DATE% %TIME%] Arguments : STEP="%1" DOMAIN="%2" DISTRO="%3" PROXY_MODE="%4" >> %LOGFILE% set WSL_USER=taleruser -set STEP=%1 -set DOMAIN=%2 -set DISTRO=%3 +set INSTALL_FAILED=0 +:: The tilde modifier below strips surrounding quotes. NSIS invokes this file +:: as: ALL "" Debian -- so a plain percent-2 yields the two quote characters, +:: which is not empty, so the localhost fallback never fired and an empty +:: --domain made taler-merchant-rproxy-setup swallow --httponly as the domain. +:: (Do not write a tilde-percent sequence in a comment: cmd still parses it.) +set STEP=%~1 +set DOMAIN=%~2 +set DISTRO=%~3 +set PROXY_MODE=%~4 set WSL=wsl.exe if exist "%WINDIR%\Sysnative\wsl.exe" set WSL=%WINDIR%\Sysnative\wsl.exe -set PROXY_MODE=%4 if "%STEP%"=="" ( set STEP=ALL @@ -87,19 +93,62 @@ if defined NEED_REBOOT ( if not "%STEP%"=="ALL" exit /b -echo [%DATE% %TIME%] Updating WSL to the latest version if needed... >> %LOGFILE% -echo [INFO] If a black window appears, please press any key when prompted, then wait until the window closes automatically. -echo [INFO] The installation will continue automatically after the update. -timeout /t 3 /nobreak - -:: Use start /wait to ensure the script waits for the update to finish (including if user has to press a key) -start /wait "" %WSL% --update +echo [%DATE% %TIME%] Checking for the WSL runtime... >> %LOGFILE% + +:: The in-box wsl.exe in System32 is only a stub. On a machine that has no WSL +:: runtime it answers "The Windows Subsystem for Linux is not installed" to +:: --update, to a bare --install and to every flagged form, so it cannot be +:: relied on to bootstrap itself. Install Microsoft's official MSI instead; +:: msiexec /quiet does it without any prompt. This only runs when WSL really +:: is missing, so machines that already have it pay nothing. +%WSL% --version >nul 2>&1 +if not errorlevel 1 goto wsl_runtime_present + +echo [%DATE% %TIME%] WSL runtime absent; installing it from the official MSI. >> %LOGFILE% +echo [INFO] Installing the Windows Subsystem for Linux, a one-time download of about 350 MB. +set "WSLMSI=%TEMP%\wsl-runtime.msi" +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0get-wsl-msi.ps1" "%WSLMSI%" >> %LOGFILE% 2>&1 +if errorlevel 1 goto wsl_msi_download_failed + +msiexec /i "%WSLMSI%" /quiet /norestart +set MSIRC=!errorlevel! +echo [%DATE% %TIME%] msiexec returned !MSIRC! >> %LOGFILE% +del "%WSLMSI%" >nul 2>&1 + +:: 3010 is ERROR_SUCCESS_REBOOT_REQUIRED. Measured on Windows 11 26H2 the MSI +:: returns 0 and WSL works at once, but honour 3010 if a build asks for it. +if !MSIRC! equ 3010 goto wsl_msi_reboot +if !MSIRC! neq 0 goto wsl_msi_failed +echo [%DATE% %TIME%] WSL runtime installed from MSI. >> %LOGFILE% +goto wsl_runtime_present + +:wsl_msi_download_failed +echo [%DATE% %TIME%] [ERREUR] Could not download the WSL runtime. >> %LOGFILE% +echo [ERREUR] Could not download the Windows Subsystem for Linux installer. +exit /b 1 + +:wsl_msi_reboot +echo [%DATE% %TIME%] [INFO] WSL installed, restart required. >> %LOGFILE% +echo [INFO] The Windows Subsystem for Linux was installed and Windows must restart. +echo [INFO] Please restart your computer and run this installer again. +exit /b 1 + +:wsl_msi_failed +echo [%DATE% %TIME%] [ERREUR] msiexec failed with !MSIRC! >> %LOGFILE% +echo [ERREUR] Installing the Windows Subsystem for Linux failed, code !MSIRC!. +exit /b 1 + +:wsl_runtime_present +%WSL% --update if not "%STEP%"=="ALL" exit /b :step2 echo [%DATE% %TIME%] [2/8] Installing %DISTRO%... >> %LOGFILE% -%WSL% --install -d %DISTRO% +:: --no-launch is essential: without it wsl.exe runs the distro's first-run +:: setup, which blocks on "Please create a default UNIX user account" with no +:: visible window. Step 3 creates %WSL_USER% directly instead. +%WSL% --install -d %DISTRO% --no-launch --web-download echo [%DATE% %TIME%] Initializing %DISTRO% first launch... >> %LOGFILE% @@ -112,30 +161,42 @@ echo [%DATE% %TIME%] DISTRO="%DISTRO%", WSL_USER="%WSL_USER%" >> %LOGFILE% %WSL% -d %DISTRO% -- bash -c "id %WSL_USER% >/dev/null 2>&1 || sudo useradd -m %WSL_USER%" echo [%DATE% %TIME%] Waiting for user "%WSL_USER%" to be present... >> %LOGFILE% +set WAIT_TRIES=0 :wait_for_taleruser %WSL% -d %DISTRO% -- bash -c "id %WSL_USER%" >nul 2>&1 if %errorlevel% neq 0 ( - timeout /t 2 >nul - goto wait_for_taleruser + ping -n 3 127.0.0.1 >nul 2>&1 + set /a WAIT_TRIES+=1 + if !WAIT_TRIES! lss 60 goto wait_for_taleruser + echo [%DATE% %TIME%] [ERREUR] User %WSL_USER% did not appear in %DISTRO% after ~2 minutes. >> %LOGFILE% + echo [ERREUR] Could not create the %WSL_USER% account inside %DISTRO%. + exit /b 1 ) echo [%DATE% %TIME%] User "%WSL_USER%" exists in Debian! >> %LOGFILE% %WSL% -d %DISTRO% -- bash -c "echo '%WSL_USER% ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/%WSL_USER%" %WSL% -d %DISTRO% -- bash -c "sudo chmod 0440 /etc/sudoers.d/%WSL_USER%" echo [%DATE% %TIME%] Sudoers file updated. >> %LOGFILE% -if not "%STEP%"=="ALL" exit /b -REM Nettoie /etc/wsl.conf avant d’ajouter -%WSL% -d %DISTRO% -- bash -c "sudo sed -i '/^\[user\]/d;/^default *=/d' /etc/wsl.conf" -%WSL% -d %DISTRO% -- bash -c "echo -e '[user]\ndefault = %WSL_USER%' | sudo tee -a /etc/wsl.conf > /dev/null" -echo [%DATE% %TIME%] wsl.conf set default user. >> %LOGFILE% +REM "wsl --manage --set-default-user" is the supported way to do this. Editing +REM [user] into /etc/wsl.conf by hand also risks clobbering the [boot] +REM systemd=true stanza the Debian image ships, which every systemctl call here +REM depends on. Fall back to the manual edit only on older wsl.exe builds. +%WSL% --manage %DISTRO% --set-default-user %WSL_USER% +if errorlevel 1 ( + echo [%DATE% %TIME%] --set-default-user unavailable, editing wsl.conf >> %LOGFILE% + %WSL% -d %DISTRO% -u root -- bash -c "sed -i '/^\[user\]/d;/^default *=/d' /etc/wsl.conf 2>/dev/null; printf '[user]\ndefault = %WSL_USER%\n' >> /etc/wsl.conf" +) +echo [%DATE% %TIME%] default WSL user set to %WSL_USER%. >> %LOGFILE% :: Pour appliquer immédiatement, termine la distro : %WSL% --terminate %DISTRO% echo [%DATE% %TIME%] Distro %DISTRO% terminated for user change. >> %LOGFILE% +if not "%STEP%"=="ALL" exit /b :step4 -echo [%DATE% %TIME%] [4/8] Adding user to www-data group... >> %LOGFILE%echo [DEBUG] DISTRO="%DISTRO%", WSL_USER="%WSL_USER%", DOMAIN="%DOMAIN%" >> %LOGFILE% +echo [%DATE% %TIME%] [4/8] Adding user to www-data group... >> %LOGFILE% +echo [DEBUG] DISTRO="%DISTRO%", WSL_USER="%WSL_USER%", DOMAIN="%DOMAIN%" >> %LOGFILE% %WSL% -d %DISTRO% -- bash -c "sudo usermod -aG www-data %WSL_USER% || echo 'Warning: Could not add user to www-data (might already be a member)'" echo [%DATE% %TIME%] User %WSL_USER% added to www-data (or already a member). >> %LOGFILE% if not "%STEP%"=="ALL" exit /b @@ -198,12 +259,15 @@ if not "%STEP%"=="ALL" exit /b echo [%DATE% %TIME%] [7/8] Restarting WSL... >> %LOGFILE% echo [DEBUG] DISTRO="%DISTRO%" >> %LOGFILE% %WSL% --shutdown -timeout /t 3 +ping -n 4 127.0.0.1 >nul 2>&1 echo [%DATE% %TIME%] Running final installation script... >> %LOGFILE% %WSL% -d %DISTRO% -- bash -c "sudo -u %WSL_USER% bash /home/%WSL_USER%/taler-installer/install-taler-from-apt.sh | tee /home/%WSL_USER%/taler-installer/install-bat.log" +set STEP7_STATUS=!errorlevel! %WSL% -d %DISTRO% -- bash -c "sudo cp /home/%WSL_USER%/taler-installer/install-bat.log /mnt/c/Users/Public/install-bat.log" -echo [%DATE% %TIME%] Final install script run, log copied to Windows. >> %LOGFILE% +echo [%DATE% %TIME%] Final install script returned !STEP7_STATUS!, log copied to Windows. >> %LOGFILE% +if not "!STEP7_STATUS!"=="0" set INSTALL_FAILED=1 +if not "!STEP7_STATUS!"=="0" echo [%DATE% %TIME%] [ERREUR] install-taler-from-apt.sh failed. >> %LOGFILE% if not "%STEP%"=="ALL" exit /b :step8 @@ -225,24 +289,34 @@ if "%PROXY_MODE%"=="" ( REM Installe Apache (dans tous les cas) echo [%DATE% %TIME%] Installation Apache2... >> %LOGFILE% %WSL% -d %DISTRO% -- bash -c "sudo apt-get update && sudo apt-get install -y apache2" >> %LOGFILE% 2>&1 +set APACHE_STATUS=!errorlevel! +if not "!APACHE_STATUS!"=="0" set INSTALL_FAILED=1 +if not "!APACHE_STATUS!"=="0" echo [%DATE% %TIME%] [ERREUR] apache2 install failed (!APACHE_STATUS!). >> %LOGFILE% if /I "%PROXY_MODE%"=="https" ( echo [%DATE% %TIME%] Mode HTTPS sélectionné pour %DOMAIN% >> %LOGFILE% echo [%DATE% %TIME%] Setting up HTTPS with Let's Encrypt for %DOMAIN% ... >> %LOGFILE% - %WSL% -d %DISTRO% -- bash -c "sudo taler-merchant-rproxy-setup --acme --merchant-url='https://%DOMAIN%:8888/' --apache" >> %LOGFILE% 2>&1 - set CERT_STATUS=%errorlevel% - echo [%DATE% %TIME%] HTTPS return code : %CERT_STATUS% >> %LOGFILE% - if not %CERT_STATUS%==0 ( + %WSL% -d %DISTRO% -- bash -c "sudo taler-merchant-rproxy-setup --domain !DOMAIN! --apache --httpsonly" >> %LOGFILE% 2>&1 + set CERT_STATUS=!errorlevel! + echo [%DATE% %TIME%] HTTPS return code : !CERT_STATUS! >> %LOGFILE% + if not "!CERT_STATUS!"=="0" set INSTALL_FAILED=1 + if not "!CERT_STATUS!"=="0" ( echo [%DATE% %TIME%] [ERREUR] Certificat HTTPS échec pour %DOMAIN% >> %LOGFILE% echo [ERREUR] Impossible d’obtenir un certificat HTTPS valide pour %DOMAIN%. echo [ERREUR] Veuillez vérifier que le domaine pointe bien sur cette machine et soit accessible publiquement. - timeout /t 5 /nobreak + ping -n 6 127.0.0.1 >nul 2>&1 ) ) else ( echo [%DATE% %TIME%] Mode HTTP sélectionné pour %DOMAIN% >> %LOGFILE% echo [%DATE% %TIME%] Setting up HTTP only for %DOMAIN% ... >> %LOGFILE% - %WSL% -d %DISTRO% -- bash -c "sudo taler-merchant-rproxy-setup --domain %DOMAIN% --httponly --apache" >> %LOGFILE% 2>&1 - echo [%DATE% %TIME%] HTTP only proxy finished for %DOMAIN% >> %LOGFILE% + %WSL% -d %DISTRO% -- bash -c "sudo taler-merchant-rproxy-setup --domain !DOMAIN! --httponly --apache" >> %LOGFILE% 2>&1 + set PROXY_STATUS=!errorlevel! + echo [%DATE% %TIME%] HTTP only proxy finished for !DOMAIN! with return code !PROXY_STATUS! >> %LOGFILE% + if not "!PROXY_STATUS!"=="0" set INSTALL_FAILED=1 + if not "!PROXY_STATUS!"=="0" ( + echo [%DATE% %TIME%] [ERREUR] Echec configuration du reverse proxy HTTP pour !DOMAIN! >> %LOGFILE% + echo [ERREUR] Impossible de configurer le reverse proxy HTTP pour !DOMAIN!. + ) ) echo [%DATE% %TIME%] --- Fin configuration reverse proxy --- >> %LOGFILE% @@ -258,8 +332,22 @@ REM Start Apache service REM Scheduled task (on next boot): Apache will start automatically powershell -Command "Register-ScheduledTask -Action (New-ScheduledTaskAction -Execute '%WINDIR%\System32\wsl.exe' -Argument '-d %DISTRO% -- bash -c \"sudo service apache2 start && while true; do sleep 3600; done\"') -Trigger (New-ScheduledTaskTrigger -AtLogOn) -TaskName 'StartTalerApacheWSL' -User '$env:USERNAME' -RunLevel Highest -Force" +echo [%DATE% %TIME%] status: step7=[!STEP7_STATUS!] apache=[!APACHE_STATUS!] proxy=[!PROXY_STATUS!] cert=[!CERT_STATUS!] failed=[!INSTALL_FAILED!] >> %LOGFILE% + +:: Only claim success when nothing above failed. This used to print +:: unconditionally -- two seconds after logging "return code 1" -- so a wizard +:: that had installed nothing still told the operator it had worked. +if !INSTALL_FAILED!==0 goto install_ok + +echo [%DATE% %TIME%] [ERREUR] GNU Taler-Merchant installation FAILED. >> %LOGFILE% +echo [%DATE% %TIME%] === [END] GNU Taler-Merchant Installer (failed) === >> %LOGFILE% +echo. +echo [ERREUR] The installation did not complete. See %LOGFILE% +ping -n 6 127.0.0.1 >nul 2>&1 +exit /b 1 + +:install_ok echo [%DATE% %TIME%] GNU Taler-Merchant installed successfully! >> %LOGFILE% echo [%DATE% %TIME%] === [END] GNU Taler-Merchant Installer === >> %LOGFILE% - -timeout /t 5 /nobreak >nul -exit /b +ping -n 6 127.0.0.1 >nul 2>&1 +exit /b 0 diff --git a/taler-installer-taler.nsi b/taler-installer-taler.nsi @@ -45,6 +45,7 @@ Page custom DBUrlPage DBUrlPageLeave Var /GLOBAL HWND_PROGRESS_LABEL Var /GLOBAL DOMAIN_INPUT +Var /GLOBAL DOMAIN_VALUE Var /GLOBAL WSL_DISTRO Var /GLOBAL RADIO_DEBIAN Var /GLOBAL RADIO_UBUNTU @@ -56,6 +57,7 @@ Var /GLOBAL DB_URL Var /GLOBAL DBCONFIG_INPUT Var /GLOBAL ASK_DB_YESNO Var /GLOBAL USE_EXTERNAL_DB +Var /GLOBAL INSTALL_OK LangString MSG_WELCOME_TITLE ${LANG_ENGLISH} "GNU Taler-Merchant backend for Windows" LangString MSG_WELCOME_TITLE ${LANG_FRENCH} "Backend GNU Taler-Merchant pour Windows" @@ -176,7 +178,7 @@ Function DomainPageLeave ${EndIf} ${If} $0 == "localhost" MessageBox MB_OK $(MSG_LOCALHOST_WARN) - StrCpy $DOMAIN_INPUT $0 + StrCpy $DOMAIN_VALUE $0 Return ${EndIf} StrCpy $1 0 @@ -184,7 +186,7 @@ Function DomainPageLeave nsExec::ExecToStack 'ping -n 1 -w 1000 $0' Pop $2 ${If} $2 == 0 - StrCpy $DOMAIN_INPUT $0 + StrCpy $DOMAIN_VALUE $0 Return ${Else} Push $0 @@ -195,7 +197,7 @@ Function DomainPageLeave Goto loop_ping ${EndIf} use_localhost: - StrCpy $DOMAIN_INPUT "localhost" + StrCpy $DOMAIN_VALUE "localhost" MessageBox MB_OK $(MSG_LOCALHOST_WARN) ${EndIf} FunctionEnd @@ -217,7 +219,7 @@ Function ReverseProxyPage Pop $RADIO_HTTPS ; Récupère la valeur du domaine - ${NSD_GetText} $DOMAIN_INPUT $1 + StrCpy $1 $DOMAIN_VALUE ${If} $1 == "localhost" ; HTTPS grisé, HTTP sélectionné @@ -246,9 +248,11 @@ FunctionEnd ; -------- SECTION INSTALL PRINCIPALE -------- Section "Install Taler-Merchant" + StrCpy $INSTALL_OK "1" SetOutPath "$INSTDIR" SetDetailsView show File "setup-wsl-debian.bat" + File "get-wsl-msi.ps1" File "install-taler-from-apt.sh" File "test-db.sh" File "set-db.sh" @@ -316,10 +320,20 @@ Section "Install Taler-Merchant" DetailPrint "[7/8] Restarting WSL and launching installation..." SendMessage $HWND_PROGRESS_LABEL ${WM_SETTEXT} 0 "STR:Progress: 70%" nsExec::ExecToLog '"$INSTDIR\setup-wsl-debian.bat" ALL "" $WSL_DISTRO' + Pop $0 + ${If} $0 != 0 + DetailPrint "ERROR: the installation step failed with code $0." + StrCpy $INSTALL_OK "0" + ${EndIf} - DetailPrint "[8/8] Configuring reverse proxy for $DOMAIN_INPUT in $REVERSE_PROXY_MODE mode..." + DetailPrint "[8/8] Configuring reverse proxy for $DOMAIN_VALUE in $REVERSE_PROXY_MODE mode..." SendMessage $HWND_PROGRESS_LABEL ${WM_SETTEXT} 0 "STR:Progress: 80%" - nsExec::ExecToLog '"$INSTDIR\setup-wsl-debian.bat" 8 $DOMAIN_INPUT $WSL_DISTRO $REVERSE_PROXY_MODE' + nsExec::ExecToLog '"$INSTDIR\setup-wsl-debian.bat" 8 $DOMAIN_VALUE $WSL_DISTRO $REVERSE_PROXY_MODE' + Pop $0 + ${If} $0 != 0 + DetailPrint "ERROR: configuring the reverse proxy failed with code $0." + StrCpy $INSTALL_OK "0" + ${EndIf} DetailPrint "Testing access to merchant reverse proxy..." nsExec::ExecToLog 'wsl -d $WSL_DISTRO -- bash -c "curl -v http://localhost"' @@ -327,14 +341,20 @@ Section "Install Taler-Merchant" Sleep 2000 DetailPrint "Checking passed domain in $WSL_DISTRO..." - nsExec::ExecToLog 'wsl -d $WSL_DISTRO -- bash -c "echo Received domain: '\''$DOMAIN_INPUT'\''"' + nsExec::ExecToLog 'wsl -d $WSL_DISTRO -- bash -c "echo Received domain: '\''$DOMAIN_VALUE'\''"' SendMessage $HWND_PROGRESS_LABEL ${WM_SETTEXT} 0 "STR:Progress: 90%" Sleep 2000 SendMessage $HWND_PROGRESS_LABEL ${WM_SETTEXT} 0 "STR:Progress: 99%" Sleep 1000 - DetailPrint "Installation complete." - SendMessage $HWND_PROGRESS_LABEL ${WM_SETTEXT} 0 "STR:Progress: 100%" + ${If} $INSTALL_OK == "1" + DetailPrint "Installation complete." + SendMessage $HWND_PROGRESS_LABEL ${WM_SETTEXT} 0 "STR:Progress: 100%" + ${Else} + DetailPrint "Installation FAILED - see C:\Users\Public\taler-bat.log" + SendMessage $HWND_PROGRESS_LABEL ${WM_SETTEXT} 0 "STR:Installation failed" + MessageBox MB_OK|MB_ICONSTOP $(MSG_INSTALL_FAILED) + ${EndIf} CreateShortCut "$SMPROGRAMS\Taler-Merchant.lnk" "$INSTDIR\taler-installer.exe" SectionEnd @@ -414,16 +434,23 @@ FunctionEnd ; ---- PAGE FINISH : open browser ---- Function .onGUIEnd -MessageBox MB_OK|MB_ICONINFORMATION "Installation is complete! Your browser will now open your domain." + ; This runs on every exit from the GUI, including a cancel and a failed run, + ; so announcing success here unconditionally told operators the install had + ; worked when it had not. Only celebrate when the section actually succeeded. + ${If} $INSTALL_OK != "1" + Return + ${EndIf} + + MessageBox MB_OK|MB_ICONINFORMATION "Installation is complete! Your browser will now open your domain." -; Force WSL à démarrer Apache AVANT le navigateur -nsExec::ExecToLog 'wsl -d $WSL_DISTRO -- bash -c "sudo service apache2 restart"' -Sleep 2000 + ; Force WSL à démarrer Apache AVANT le navigateur + nsExec::ExecToLog 'wsl -d $WSL_DISTRO -- bash -c "sudo service apache2 restart"' + Sleep 2000 ${If} $REVERSE_PROXY_MODE == "https" - ExecShell "open" "https://$DOMAIN_INPUT" + ExecShell "open" "https://$DOMAIN_VALUE" ${Else} - ExecShell "open" "http://$DOMAIN_INPUT" + ExecShell "open" "http://$DOMAIN_VALUE" ${EndIf} FunctionEnd diff --git a/test-db.sh b/test-db.sh @@ -1,6 +1,7 @@ #!/bin/bash # test-db.sh # Usage: ./test-db.sh <POSTGRES_URL> +mkdir -p /home/taleruser/taler-installer 2>/dev/null echo "=== [DEBUG] test-db.sh called ===" | tee -a /home/taleruser/taler-installer/test-db.log echo "Received URL: $1" | tee -a /home/taleruser/taler-installer/test-db.log POSTGRES_URL="$1"