From 36e09036f14c0d5beeda7e070b4866d1baa65cb5 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 19:21:56 -0400 Subject: [PATCH 01/16] Add the design of Linux support, step 2 (Linux package) Co-Authored-By: Claude Opus 5.5 --- specs/2026-10-04-linux-package-design.md | 162 +++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 specs/2026-10-04-linux-package-design.md diff --git a/specs/2026-10-04-linux-package-design.md b/specs/2026-10-04-linux-package-design.md new file mode 100644 index 0000000..602402d --- /dev/null +++ b/specs/2026-10-04-linux-package-design.md @@ -0,0 +1,162 @@ +# Linux support, step 2: a Linux package in release 1.5.0.0 + +Date: 2026-10-04. Status: approved design. + +## Context + +- Step 1 is merged (PR #7): the `net10.0` target of Canon.API runs on Linux x64 from the sources, verified with an EOS + R100. See `specs/2026-10-04-linux-edsdk-design.md`. +- Release 1.5.0.0 must ship a Linux build next to the Windows package. +- Today `.github/workflows/build.yml` runs one Windows job on a `v*` tag: tag check, version sync, build, tests, + `CanonWebAPI.zip` (win-x64), GitHub release, then `docs/autoupdate.xml`, which the photo booths read to update + themselves. The Windows package name, content and feed must not change. +- Decisions: x64 only; a `.tar.gz` usable by hand plus an optional `install.sh` (systemd service, USB permissions); + manual updates on Linux; an atomic release pipeline. +- Verified on the development machine (Ubuntu 26.04): `dotnet publish -f net10.0 -r linux-x64 --self-contained true + -p:PublishSingleFile=true` produces an executable `Canon.API` that needs no .NET native library next to it, only + `libEDSDK.so`. Without a camera, `/status` answers `"error": "No Canon camera detected: Device not found"`. SIGTERM + stops it cleanly in 2 s (the camera session is closed). The single file requires glibc 2.27 or later; `libEDSDK.so` + requires glibc 2.14 and libstdc++ (GLIBCXX 3.4.21), plus `libusb-1.0` and `libudev`. + +## Goals + +- Every release has a second asset, `CanonWebAPI-linux-x64.tar.gz`, built and checked in the same workflow as the + Windows package. +- On Linux the API runs by hand from the extracted folder, or as a systemd service installed by `install.sh`. +- A release is published only when both packages built, passed the tests, and started and loaded the EDSDK. +- The pull request CI starts both packages too (the published Windows executable was never run in CI). +- Release 1.5.0.0 is prepared (version, upgrade note, changelog); the tag is pushed only with the user's go-ahead. + +## Non-goals + +ARM (arm64, arm32), automatic update on Linux, `.deb` or other distribution packages, a Linux camera test script +(`tools/Test-Camera.ps1` needs PowerShell), Docker images for Linux. + +## Design + +### 1. The Linux package + +- Asset: `CanonWebAPI-linux-x64.tar.gz`, containing one folder `CanonWebAPI/`: + - `Canon.API` (self-contained single-file executable), `libEDSDK.so`, `appsettings.json`, + `appsettings.Development.json`, the `.pdb` files and `Canon.API.staticwebassets.endpoints.json` (as in the + Windows package); + - `install.sh`, `canonwebapi.service`, `60-canonwebapi.rules` (section 2). +- Built with `dotnet publish Canon.API/Canon.API.csproj -c Release -f net10.0 -r linux-x64 --self-contained true + -p:PublishSingleFile=true`. `EnableCompressionInSingleFile` applies to Linux as to Windows. No + `IncludeNativeLibrariesForSelfExtract`: nothing would be extracted (verified above), and a service user has no home + directory to extract to. +- The package content is defined in `Canon.API.csproj`, like the Windows DLLs: for the `net10.0` target, the files of + `packaging/linux/` are copied to the publish folder only (not to the build output). `install.sh` keeps its + executable bit through the tar archive, which is created on Linux. +- Run by hand: `./Canon.API` listens on `http://localhost:5000`, like the Windows executable; `--urls` or + `ASPNETCORE_URLS` change it. Needs `libusb-1.0`, read/write access to the camera USB device (desktop session, or the + udev rule of `install.sh`), and no other program holding the camera. +- The Windows package is unchanged. + +### 2. `install.sh` + +Run as root from the extracted folder: `sudo ./install.sh` installs or updates, `sudo ./install.sh --uninstall` +removes. Files in `packaging/linux/`: `install.sh`, `canonwebapi.service`, `60-canonwebapi.rules`. + +Install or update: + +1. Checks: root, `x86_64`, systemd present, `libusb-1.0.so.0` known to `ldconfig`. When libusb is missing, prints + `sudo apt install libusb-1.0-0` and stops (no package installed by the script). +2. Creates the system user `canonwebapi` when missing: no login shell, no home directory. +3. Stops the service when it runs (update). +4. Copies the program files to `/opt/canonwebapi`, owned by root. `logs/` is owned by `canonwebapi`. An existing + `appsettings.Local.json` is never overwritten or removed; none is created. +5. Installs `/etc/udev/rules.d/60-canonwebapi.rules`, then reloads udev and triggers Canon devices: + `SUBSYSTEM=="usb", ATTR{idVendor}=="04a9", MODE="0660", GROUP="canonwebapi"`. Desktop users keep their own access + (the `uaccess` ACL is separate from the group). +6. Installs `/etc/systemd/system/canonwebapi.service`, reloads systemd, enables and (re)starts the service, then + prints its status and the URL. + +Service unit: + +```ini +[Unit] +Description=CanonWebAPI (Canon camera web API) +After=network.target + +[Service] +User=canonwebapi +Group=canonwebapi +WorkingDirectory=/opt/canonwebapi +ExecStart=/opt/canonwebapi/Canon.API +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +systemd stops it with SIGTERM: ASP.NET Core shuts down cleanly and closes the camera session. Logs: the API files in +`/opt/canonwebapi/logs/` and `journalctl -u canonwebapi` (console output). + +Uninstall: stops and disables the service, removes the unit, the udev rule, `/opt/canonwebapi` (settings and logs +included) and the `canonwebapi` user. + +Update: extract the new archive, run its `install.sh`. + +### 3. Release workflow and CI + +A PowerShell script, `tools/Test-Package.ps1`, checks a published package: it starts the executable with +`--urls http://localhost:5000 --AutoUpdate:Enabled=false`, polls `GET /status` for up to 60 s, and passes when the +answer is HTTP 200 with an `error` containing `No Canon camera detected` (the runners have no camera; a missing or +unloadable `EDSDK.dll` / `libEDSDK.so` gives another error). It stops the executable and prints its output on failure. +PowerShell 7 is installed on the GitHub Windows and Ubuntu runners. + +`.github/workflows/build.yml` (tag `v*`), jobs: + +1. `prepare` (ubuntu-latest): the current tag check and version sync steps (PowerShell, `shell: pwsh`); outputs the + commit to build and the version. +2. `windows` (windows-latest) and `linux` (ubuntu-latest), in parallel, both on the prepared commit: restore, build, + test, publish their runtime, `Test-Package.ps1`, then archive (`CanonWebAPI.zip` with `Compress-Archive` as today, + `CanonWebAPI-linux-x64.tar.gz` with `tar`) and upload it as a workflow artifact. +3. `release` (ubuntu-latest), when both succeeded: downloads the two archives, creates the GitHub release with both + (generated notes, as today), then writes and commits `docs/autoupdate.xml` (Windows zip URL, as today). + +When the Windows or Linux job fails, nothing is released and the feed is not updated. + +`.github/workflows/ci.yml` (pull requests and `main`): the Windows job also publishes win-x64 and runs +`Test-Package.ps1`; a new Linux job (ubuntu-latest) restores, builds, tests, publishes linux-x64 and runs +`Test-Package.ps1`. + +### 4. Documentation + +- README: + - an "Installing on Linux" section: download and extract, prerequisites (`libusb-1.0`, glibc 2.27+, systemd for the + service), run by hand, `install.sh` (what it installs), update, uninstall, settings in + `/opt/canonwebapi/appsettings.Local.json`, logs, `systemctl status|restart canonwebapi`, `journalctl -u + canonwebapi`, and the desktop automount note (gvfs can hold the camera); + - "Running on Linux" stays the from-source section; + - System Requirements and the release steps mention both packages. +- CLAUDE.md: the two packages, `packaging/linux/`, `tools/Test-Package.ps1`, the release pipeline jobs. +- CHANGELOG (Unreleased): Linux package and `install.sh`; release pipeline and CI check both packages. + +### 5. Release 1.5.0.0 + +Last step, after the implementation: + +- Version 1.5.0.0 in `Canon.API/Canon.API.csproj`, the README ("Current version") and `docs/openapi.json`. +- README "Upgrading from 1.4.x" note: the flash setting is no longer written while the live view runs (a value set with + `POST /flash` stays until the live view stops); `dotnet run` and `dotnet publish` need `-f`; Linux package. +- CHANGELOG: `[Unreleased]` becomes `[1.5.0.0] - `, with a new empty `[Unreleased]` and updated links. +- The tag `v1.5.0.0` is pushed only with the user's go-ahead: it updates every photo booth. + +## Testing + +- Pull request CI: both packages start and load the EDSDK (`Test-Package.ps1`), on Windows and Linux runners. +- Development machine: build the Linux package with the release commands and extract it; run `./Canon.API` by hand; + run `install.sh` (sudo, by the user), check the service, the udev rule and the logs; camera test with the R100 + against the service (captures with the live view, flash change, power cycle); `install.sh --uninstall`. + +## Risks + +- The GitHub Ubuntu runner may not let the EDSDK initialize (no USB). `/status` would then report another error and + `Test-Package.ps1` would fail on the pull request, before any release: the expected message is adjusted there, still + rejecting library load failures. +- On a desktop, gvfs can mount the camera and hold it: documented (unmount it or disable the automount). +- `install.sh` changes the system (user, `/opt`, udev rule, service): it is tested on the development machine with + the user, then uninstalled. From b05d8c6da3817bb7aa8024cd41f782d420697144 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 19:28:48 -0400 Subject: [PATCH 02/16] Add the implementation plan of Linux support, step 2 Co-Authored-By: Claude Opus 5.5 --- specs/2026-10-04-linux-package-design.md | 11 +- specs/2026-10-04-linux-package-plan.md | 893 +++++++++++++++++++++++ 2 files changed, 900 insertions(+), 4 deletions(-) create mode 100644 specs/2026-10-04-linux-package-plan.md diff --git a/specs/2026-10-04-linux-package-design.md b/specs/2026-10-04-linux-package-design.md index 602402d..9c27d47 100644 --- a/specs/2026-10-04-linux-package-design.md +++ b/specs/2026-10-04-linux-package-design.md @@ -102,9 +102,10 @@ Update: extract the new archive, run its `install.sh`. ### 3. Release workflow and CI A PowerShell script, `tools/Test-Package.ps1`, checks a published package: it starts the executable with -`--urls http://localhost:5000 --AutoUpdate:Enabled=false`, polls `GET /status` for up to 60 s, and passes when the -answer is HTTP 200 with an `error` containing `No Canon camera detected` (the runners have no camera; a missing or -unloadable `EDSDK.dll` / `libEDSDK.so` gives another error). It stops the executable and prints its output on failure. +`--urls http://localhost:5000 --AutoUpdate:Enabled=false` from a temporary copy of its folder (its logs stay out of +the package), polls `GET /status` for up to 60 s, and passes when the answer is HTTP 200 with `connected: true` or an +`error` containing `No Canon camera detected` (the runners have no camera; a missing or unloadable `EDSDK.dll` / +`libEDSDK.so` gives another error). It stops the executable and prints its output on failure. PowerShell 7 is installed on the GitHub Windows and Ubuntu runners. `.github/workflows/build.yml` (tag `v*`), jobs: @@ -117,7 +118,9 @@ PowerShell 7 is installed on the GitHub Windows and Ubuntu runners. 3. `release` (ubuntu-latest), when both succeeded: downloads the two archives, creates the GitHub release with both (generated notes, as today), then writes and commits `docs/autoupdate.xml` (Windows zip URL, as today). -When the Windows or Linux job fails, nothing is released and the feed is not updated. +When the Windows or Linux job fails, nothing is released and the feed is not updated. The workflow can also be run by +hand from the Actions tab (`workflow_dispatch`): a dry run of `prepare` (no tag check, no version commit), `windows` and +`linux` that publishes nothing, to check the pipeline on `main` before tagging. `.github/workflows/ci.yml` (pull requests and `main`): the Windows job also publishes win-x64 and runs `Test-Package.ps1`; a new Linux job (ubuntu-latest) restores, builds, tests, publishes linux-x64 and runs diff --git a/specs/2026-10-04-linux-package-plan.md b/specs/2026-10-04-linux-package-plan.md new file mode 100644 index 0000000..8d0e16b --- /dev/null +++ b/specs/2026-10-04-linux-package-plan.md @@ -0,0 +1,893 @@ +# Linux Package (Release 1.5.0.0) - Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship `CanonWebAPI-linux-x64.tar.gz` (with an optional systemd installer) next to the Windows package in every release, both built and checked by an atomic release pipeline, then prepare release 1.5.0.0. + +**Architecture:** The package content is defined in `Canon.API.csproj` for the `net10.0` target (publish-only files from `packaging/linux/`). `tools/Test-Package.ps1` starts a published package and checks that it loads the EDSDK; the CI and release workflows run it on Windows and Linux runners. The release workflow becomes prepare -> windows + linux (parallel) -> release. + +**Tech Stack:** .NET 10 (SDK 10.0.112 here), GitHub Actions (windows-latest, ubuntu-latest), PowerShell 7 (`pwsh`, on both runners), bash, systemd, udev. + +**Spec:** `specs/2026-10-04-linux-package-design.md` + +## Global Constraints + +- Branch `feature/linux-package`. Every commit message ends with `Co-Authored-By: Claude Opus 5.5 `. +- The Windows package does not change: asset `CanonWebAPI.zip`, same content, same `docs/autoupdate.xml` content and URL. +- Linux asset: `CanonWebAPI-linux-x64.tar.gz`, one top folder `CanonWebAPI/`. x64 only. +- Linux publish command: `dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0 --runtime linux-x64 --self-contained true -p:PublishSingleFile=true`. +- Install locations: `/opt/canonwebapi`, user and group `canonwebapi`, `/etc/systemd/system/canonwebapi.service`, `/etc/udev/rules.d/60-canonwebapi.rules`. API URL `http://localhost:5000`. +- Package check: `/status` answers with `connected: true` or an `error` containing `No Canon camera detected`. +- Code comments in English, senior altitude (only what the code cannot say). ASCII punctuation in code, comments, scripts and docs. +- Every change is recorded in the `Unreleased` section of `CHANGELOG.md`. +- PowerShell 7 for local checks: `/tmp/claude-1000/-home-gilbert-Documents-Project-CanonWebAPI/055083d5-ab07-4905-b425-34b4a1057fc9/scratchpad/pwsh/pwsh` (not installed system wide). +- Verified before this plan: with the Task 1 project settings, `dotnet build CanonSDK.sln` has 0 warnings, the `packaging/linux` files are not copied to the build output, and the linux-x64 publish folder contains `Canon.API` (executable, 53 MB compressed), `libEDSDK.so`, the three packaging files (`install.sh` keeps its executable bit), `appsettings*.json`, `*.pdb`, `Canon.API.staticwebassets.endpoints.json`. + +## File Structure + +| File | Change | +|------|--------| +| `Canon.API/Canon.API.csproj` | Linux publish settings and packaging items for `net10.0` | +| `packaging/linux/install.sh` | New: install, update, uninstall (executable) | +| `packaging/linux/canonwebapi.service` | New: systemd unit | +| `packaging/linux/60-canonwebapi.rules` | New: udev rule | +| `tools/Test-Package.ps1` | New: starts a published package and checks `/status` | +| `.github/workflows/ci.yml` | Windows job checks its package; new Linux job | +| `.github/workflows/build.yml` | prepare / windows / linux / release jobs; manual dry run | +| `README.md`, `CLAUDE.md`, `CHANGELOG.md` | Linux installation, CI, release pipeline; release 1.5.0.0 | +| `docs/openapi.json` | Version 1.5.0 (Task 6) | + +--- + +### Task 1: Linux package content and installer + +**Files:** +- Create: `packaging/linux/install.sh`, `packaging/linux/canonwebapi.service`, `packaging/linux/60-canonwebapi.rules` +- Modify: `Canon.API/Canon.API.csproj`, `README.md`, `CHANGELOG.md` + +**Interfaces:** +- Produces: a linux-x64 publish folder containing `Canon.API`, `libEDSDK.so`, `install.sh` (executable), `canonwebapi.service`, `60-canonwebapi.rules` at its root. `install.sh` usage: `sudo ./install.sh` (install or update), `sudo ./install.sh --uninstall`. + +- [ ] **Step 1: Check that the packaging files are not published yet** + +```bash +O=$(mktemp -d) +dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0 --runtime linux-x64 --self-contained true -p:PublishSingleFile=true --output "$O" +ls "$O" +``` + +Expected: no `install.sh`, `canonwebapi.service` or `60-canonwebapi.rules`; `Canon.API` is about 100 MB (not compressed). + +- [ ] **Step 2: The systemd unit** + +Create `packaging/linux/canonwebapi.service`: + +```ini +[Unit] +Description=CanonWebAPI (Canon camera web API) +After=network.target + +[Service] +User=canonwebapi +Group=canonwebapi +WorkingDirectory=/opt/canonwebapi +ExecStart=/opt/canonwebapi/Canon.API +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +- [ ] **Step 3: The udev rule** + +Create `packaging/linux/60-canonwebapi.rules`: + +``` +# CanonWebAPI: Canon cameras (USB vendor 04a9) can be opened by the canonwebapi service user. +SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="04a9", MODE="0660", GROUP="canonwebapi" +``` + +- [ ] **Step 4: The installer** + +Create `packaging/linux/install.sh`, then `chmod 755 packaging/linux/install.sh` (git records the executable bit): + +```bash +#!/usr/bin/env bash +# Installs or updates CanonWebAPI as a systemd service from an extracted Linux package, or removes it. +# sudo ./install.sh install or update (appsettings.Local.json and the logs are kept) +# sudo ./install.sh --uninstall remove the service, the udev rule, /opt/canonwebapi and the canonwebapi user +set -euo pipefail + +readonly app_dir=/opt/canonwebapi +readonly service=canonwebapi +readonly user=canonwebapi +readonly unit_file=/etc/systemd/system/canonwebapi.service +readonly rule_file=/etc/udev/rules.d/60-canonwebapi.rules +package_dir="$(cd "$(dirname "$0")" && pwd)" +readonly package_dir + +fail() { + echo "Error: $*" >&2 + exit 1 +} + +uninstall() { + systemctl disable --now "$service" 2>/dev/null || true + rm -f "$unit_file" "$rule_file" + systemctl daemon-reload + udevadm control --reload + rm -rf "$app_dir" + if id "$user" >/dev/null 2>&1; then + userdel "$user" + fi + echo "CanonWebAPI removed." +} + +install_package() { + [ "$(uname -m)" = x86_64 ] || fail "this package is for x86_64, not $(uname -m)." + # No grep -q: with pipefail, grep exiting early would fail the pipeline through SIGPIPE. + ldconfig -p | grep 'libusb-1.0.so.0' >/dev/null || fail "libusb-1.0 is missing: sudo apt install libusb-1.0-0" + [ -f "$package_dir/Canon.API" ] || fail "Canon.API not found next to the script: run it from the extracted package." + [ "$package_dir" != "$app_dir" ] || fail "run the install.sh of the extracted package, not the installed one." + + if ! id "$user" >/dev/null 2>&1; then + useradd --system --user-group --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin "$user" + fi + + systemctl stop "$service" 2>/dev/null || true + + install -d -o root -g root -m 755 "$app_dir" + install -d -o "$user" -g "$user" -m 755 "$app_dir/logs" + + # Program files only: appsettings.Local.json and the logs of the installation are kept. + for file in "$package_dir"/*; do + name="$(basename "$file")" + case "$name" in + appsettings.Local.json | canonwebapi.service | 60-canonwebapi.rules) continue ;; + esac + [ -f "$file" ] || continue + install -o root -g root -m 644 "$file" "$app_dir/$name" + done + chmod 755 "$app_dir/Canon.API" "$app_dir/install.sh" + + install -o root -g root -m 644 "$package_dir/60-canonwebapi.rules" "$rule_file" + udevadm control --reload + # Applies the rule to a camera that is already plugged in. + udevadm trigger --subsystem-match=usb --attr-match=idVendor=04a9 + + install -o root -g root -m 644 "$package_dir/canonwebapi.service" "$unit_file" + systemctl daemon-reload + systemctl enable "$service" + systemctl restart "$service" + + echo "CanonWebAPI is installed in $app_dir and runs as the $service service: http://localhost:5000/status" + echo "Settings: $app_dir/appsettings.Local.json. Logs: $app_dir/logs and journalctl -u $service" + systemctl --no-pager status "$service" || true +} + +[ "$(id -u)" -eq 0 ] || fail "run it as root: sudo $0 $*" +command -v systemctl >/dev/null || fail "systemd is required." + +case "${1:-}" in + "") install_package ;; + --uninstall) uninstall ;; + *) fail "unknown option $1. Usage: sudo $0 [--uninstall]" ;; +esac +``` + +- [ ] **Step 5: Publish settings and packaging items** + +In `Canon.API/Canon.API.csproj`, after the `PropertyGroup` conditioned on `net10.0-windows` (Self-Contained Single-File Deployment), add: + +```xml + + + true + +``` + +(XML comments cannot contain `--`: do not put command line options in them.) + +After the `ItemGroup` that copies `..\EDSDK\linux-x64\libEDSDK.so` for `net10.0`, add: + +```xml + + + + +``` + +- [ ] **Step 6: Check the package content** + +```bash +dotnet build CanonSDK.sln +ls Canon.API/bin/Debug/net10.0/ | grep -c -E 'install.sh|canonwebapi' # 0: publish only +O=$(mktemp -d) +dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0 --runtime linux-x64 --self-contained true -p:PublishSingleFile=true --output "$O" +ls -l "$O" +bash -n packaging/linux/install.sh && echo "syntax OK" +dotnet test CanonSDK.sln +``` + +Expected: build 0 warnings, 0 errors; the grep count is 0; the publish folder lists `Canon.API` (executable, about 53 MB), `libEDSDK.so`, `install.sh` (executable), `canonwebapi.service`, `60-canonwebapi.rules`, `appsettings.json`, `appsettings.Development.json`, the `.pdb` files and `Canon.API.staticwebassets.endpoints.json`; `syntax OK`; tests `Failed: 0`. If `shellcheck` can be installed without root (e.g. `pip install --user shellcheck-py`), run `shellcheck packaging/linux/install.sh` and fix its warnings; otherwise say so in the report. + +- [ ] **Step 7: Documentation** + +`README.md`: after the "## Automatic Updates" section (and its last line "Linux has no automatic update yet..."), add: + +````markdown +## Installing on Linux + +Each release also provides `CanonWebAPI-linux-x64.tar.gz`: a self-contained executable for Linux x64 (no .NET to +install) with the Linux version of the EDSDK. Requirements: glibc 2.27 or later (e.g. Ubuntu 18.04, Debian 10 or +later), `libusb-1.0` (`sudo apt install libusb-1.0-0`), and systemd for the service. There is no automatic update on +Linux. + +```bash +tar -xzf CanonWebAPI-linux-x64.tar.gz +cd CanonWebAPI +./Canon.API # by hand: http://localhost:5000 +sudo ./install.sh # or as a service started at boot +``` + +`install.sh` copies the files to `/opt/canonwebapi`, creates the `canonwebapi` system user, adds a udev rule that lets +it open Canon cameras (USB vendor `04a9`), then installs, enables and starts the `canonwebapi` systemd service. + +* Settings: `/opt/canonwebapi/appsettings.Local.json` (kept by updates), then `sudo systemctl restart canonwebapi`. +* Logs: `/opt/canonwebapi/logs/` and `journalctl -u canonwebapi`; state: `systemctl status canonwebapi`. +* Update: extract the new archive and run its `install.sh`. +* Uninstall: `sudo /opt/canonwebapi/install.sh --uninstall` removes the service, the udev rule, `/opt/canonwebapi` + (settings and logs included) and the `canonwebapi` user. +* Run by hand, the API needs read/write access to the camera USB device: desktop sessions get it, otherwise install + the service. A desktop can mount the camera (GNOME: gvfs) and hold it: unmount it (`gio mount -l`, then + `gio mount -u `) or disable the automount. +```` + +In "### Running on Linux", replace "There is no automatic update and no release package for Linux yet: run it from the sources." with "To run a release, see [Installing on Linux](#installing-on-linux); this section runs it from the sources." + +In "### System Requirements", replace the OS line with: + +```markdown +* **OS**: Windows 10/11 (x64): `CanonWebAPI.zip`; Linux x64 (glibc 2.27+, `libusb-1.0`): `CanonWebAPI-linux-x64.tar.gz` (see [Installing on Linux](#installing-on-linux)) +``` + +`CHANGELOG.md`, `## [Unreleased]` / `### Added`, first bullet: + +```markdown +- Linux package in every release: `CanonWebAPI-linux-x64.tar.gz`, a self-contained executable for Linux x64 with the + Linux EDSDK, and `install.sh`, which installs it as a systemd service with the USB access it needs + (`/opt/canonwebapi`, `canonwebapi` user, udev rule); `install.sh --uninstall` removes it. +``` + +- [ ] **Step 8: Commit** + +```bash +git add packaging/linux Canon.API/Canon.API.csproj README.md CHANGELOG.md +git commit -m "Add the Linux package content and its systemd installer + +Co-Authored-By: Claude Opus 5.5 " +git ls-files -s packaging/linux/install.sh # mode 100755 +``` + +--- + +### Task 2: Package check script and CI on Windows and Linux + +**Files:** +- Create: `tools/Test-Package.ps1` +- Modify: `.github/workflows/ci.yml`, `CLAUDE.md`, `CHANGELOG.md` + +**Interfaces:** +- Consumes: the Linux publish folder of Task 1. +- Produces: `./tools/Test-Package.ps1 -Executable [-Url http://localhost:5000] [-TimeoutSeconds 60]`; exit code 0 when the package loads the EDSDK, 1 otherwise. Used by Task 3. + +- [ ] **Step 1: The script** + +Create `tools/Test-Package.ps1`: + +```powershell +<# +.SYNOPSIS + Checks that a published CanonWebAPI package starts and loads the Canon EDSDK. + +.DESCRIPTION + Copies the folder of the executable to a temporary folder, so that its logs do not end up in the package, starts + it, polls GET /status and passes when the API answers with a connected camera or "No Canon camera detected" (no + camera on the machine). Any other answer, such as an EDSDK library that cannot be loaded, fails. Used by the CI and + release workflows on the Windows and Linux runners (PowerShell 7). + +.EXAMPLE + ./tools/Test-Package.ps1 -Executable ./publish/Canon.API.exe +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$Executable, + [string]$Url = 'http://localhost:5000', + [int]$TimeoutSeconds = 60 +) + +$ErrorActionPreference = 'Stop' + +$source = (Resolve-Path $Executable).Path +$work = Join-Path ([System.IO.Path]::GetTempPath()) ('canonwebapi-package-' + [guid]::NewGuid()) +Copy-Item (Split-Path $source) $work -Recurse +$exe = Join-Path $work (Split-Path $source -Leaf) +if ($IsLinux) { & chmod 755 $exe } +$output = Join-Path $work 'console.log' +$errorOutput = Join-Path $work 'console-error.log' + +$process = Start-Process $exe -WorkingDirectory $work -PassThru -RedirectStandardOutput $output ` + -RedirectStandardError $errorOutput -ArgumentList @("--urls=$Url", '--AutoUpdate:Enabled=false') +$status = $null + +try { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while (-not $status -and -not $process.HasExited -and (Get-Date) -lt $deadline) { + try { + $status = Invoke-RestMethod "$Url/status" -TimeoutSec 10 + } + catch { + Start-Sleep -Seconds 1 + } + } +} +finally { + if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force } + $process.WaitForExit() +} + +if ($status -and ($status.connected -or "$($status.error)" -match 'No Canon camera detected')) { + $detail = if ($status.connected) { "camera $($status.cameraName) connected" } else { $status.error } + Write-Host "Package OK: $detail" + Remove-Item $work -Recurse -Force + exit 0 +} + +$answer = if ($status) { $status | ConvertTo-Json -Compress } else { 'no answer' } +Write-Host "Package check failed. /status: $answer" +Write-Host '--- application output ---' +Get-Content $output, $errorOutput -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ } +exit 1 +``` + +- [ ] **Step 2: Check it locally (pass, then fail)** + +```bash +PWSH=/tmp/claude-1000/-home-gilbert-Documents-Project-CanonWebAPI/055083d5-ab07-4905-b425-34b4a1057fc9/scratchpad/pwsh/pwsh +O=$(mktemp -d) +dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0 --runtime linux-x64 --self-contained true -p:PublishSingleFile=true --output "$O" +$PWSH -NoProfile -File tools/Test-Package.ps1 -Executable "$O/Canon.API" -Url http://localhost:5098; echo "exit $?" +ls "$O" # no logs folder: the check ran in a copy +B=$(mktemp -d); cp -a "$O"/. "$B"/; rm "$B/libEDSDK.so" +$PWSH -NoProfile -File tools/Test-Package.ps1 -Executable "$B/Canon.API" -Url http://localhost:5098; echo "exit $?" +``` + +Expected: the first run prints `Package OK: No Canon camera detected: Device not found` (or `camera Canon EOS R100 connected` when a camera is plugged in and free) and exit 0; the publish folder has no `logs`; the second run prints `Package check failed` with an `/status` error about loading `EDSDK` (`Unable to load shared library 'EDSDK'...`) and exit 1. Do not leave any `Canon.API` process running (`ss -ltn | grep 5098` prints nothing). + +- [ ] **Step 3: CI on Windows and Linux** + +Replace the whole `.github/workflows/ci.yml` with: + +```yaml +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore dependencies + run: dotnet restore CanonSDK.sln + + - name: Build solution + run: dotnet build CanonSDK.sln --configuration Release --no-restore + + - name: Run unit tests + run: dotnet test CanonSDK.sln --configuration Release --no-build --logger trx --results-directory TestResults + + - name: Publish the Windows package + run: dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0-windows --runtime win-x64 --self-contained true -p:PublishSingleFile=true --output ./publish + + - name: Check that the Windows package starts and loads the EDSDK + run: ./tools/Test-Package.ps1 -Executable ./publish/Canon.API.exe + shell: pwsh + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-windows + path: TestResults + + build-and-test-linux: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Install libusb (needed by the Linux EDSDK) + run: sudo apt-get update && sudo apt-get install -y libusb-1.0-0 + + - name: Restore dependencies + run: dotnet restore CanonSDK.sln + + - name: Build solution + run: dotnet build CanonSDK.sln --configuration Release --no-restore + + - name: Run unit tests + run: dotnet test CanonSDK.sln --configuration Release --no-build --logger trx --results-directory TestResults + + - name: Publish the Linux package + run: dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0 --runtime linux-x64 --self-contained true -p:PublishSingleFile=true --output ./publish + + - name: Check that the Linux package starts and loads the EDSDK + run: ./tools/Test-Package.ps1 -Executable ./publish/Canon.API + shell: pwsh + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-linux + path: TestResults +``` + +Check the syntax: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml')); print('ci.yml OK')"` + +- [ ] **Step 4: Documentation** + +`CLAUDE.md`, section "## Testing", add a bullet after the Canon.API.Tests one: + +```markdown +- CI (pull requests and `main`): build and tests on Windows and Linux runners, then both packages are published and started by `tools/Test-Package.ps1`, which checks that they load the EDSDK (`/status`: "No Canon camera detected") +``` + +`CHANGELOG.md`, `## [Unreleased]` / `### Changed`, add: + +```markdown +- CI builds and tests on Windows and Linux, then publishes the Windows and Linux packages and starts them to check that + they load the EDSDK (`tools/Test-Package.ps1`). The published Windows executable was never run before a release. +``` + +- [ ] **Step 5: Commit** + +```bash +git add tools/Test-Package.ps1 .github/workflows/ci.yml CLAUDE.md CHANGELOG.md +git commit -m "Check both packages in CI on Windows and Linux runners + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 3: Atomic release pipeline + +**Files:** +- Modify: `.github/workflows/build.yml`, `README.md`, `CLAUDE.md`, `CHANGELOG.md` + +**Interfaces:** +- Consumes: `tools/Test-Package.ps1` (Task 2), the Linux package content (Task 1). +- Produces: on a `v*` tag, a GitHub release with `CanonWebAPI.zip` and `CanonWebAPI-linux-x64.tar.gz`, then `docs/autoupdate.xml` committed to `main`; from the Actions tab (`workflow_dispatch`), a dry run that builds, tests and checks both packages and publishes nothing. + +- [ ] **Step 1: The workflow** + +Replace the whole `.github/workflows/build.yml` with: + +```yaml +name: Release CanonWebAPI + +on: + push: + tags: + - 'v*' + # Dry run from the Actions tab: builds, tests and checks both packages of the selected branch; publishes nothing. + workflow_dispatch: + +permissions: + contents: write + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + sha: ${{ steps.commit.outputs.SHA }} + version: ${{ steps.version.outputs.VERSION }} + + steps: + # A release is built from main: this workflow commits the version and the AutoUpdater XML to it. + - name: Checkout + uses: actions/checkout@v5 + with: + ref: ${{ github.event_name == 'push' && 'main' || github.ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + # Refuse to release if main is not the tagged commit, so the packages always match the tag. + - name: Check that the tag points to main + if: github.event_name == 'push' + shell: pwsh + run: | + $mainSha = git rev-parse HEAD + $tagSha = "${{ github.sha }}" + if ($mainSha -ne $tagSha) { + Write-Error "Tag ${{ github.ref_name }} points to $tagSha but main is at $mainSha. Tag the latest commit of main." + exit 1 + } + echo "Tag ${{ github.ref_name }} matches main ($mainSha)" + + - name: Get the version + id: version + shell: pwsh + run: | + if ("${{ github.event_name }}" -eq "push") { + $version = "${{ github.ref_name }}" -replace '^v', '' + } + else { + $version = [regex]::Match((Get-Content Canon.API/Canon.API.csproj -Raw), '(.*)').Groups[1].Value + } + echo "VERSION=$version" >> $env:GITHUB_OUTPUT + echo "Version: $version" + + - name: Check and update project version + if: github.event_name == 'push' + id: version_check + shell: pwsh + run: | + $tagVersion = "${{ steps.version.outputs.VERSION }}" + + # Read current version from project file + $projectFile = "Canon.API/Canon.API.csproj" + $projectContent = Get-Content $projectFile + $currentVersion = "" + + foreach ($line in $projectContent) { + if ($line -match '(.*)') { + $currentVersion = $matches[1] + break + } + } + + echo "Current project version: $currentVersion" + echo "Tag version: $tagVersion" + + if ($currentVersion -ne $tagVersion) { + echo "VERSION_MISMATCH=true" >> $env:GITHUB_OUTPUT + echo "Versions do not match - updating project files" + + # Update Canon.API project file + $projectContent -replace '.*', "$tagVersion" -replace '.*', "$tagVersion" -replace '.*', "$tagVersion" | Set-Content $projectFile + + # Update Canon.Core project file if it has version info + $coreProjectFile = "Canon.Core/Canon.Core.csproj" + if (Test-Path $coreProjectFile) { + $coreContent = Get-Content $coreProjectFile + if ($coreContent -match '') { + $coreContent -replace '.*', "$tagVersion" -replace '.*', "$tagVersion" -replace '.*', "$tagVersion" | Set-Content $coreProjectFile + } + } + } else { + echo "VERSION_MISMATCH=false" >> $env:GITHUB_OUTPUT + echo "Versions match - no update needed" + } + + - name: Commit version updates + if: github.event_name == 'push' && steps.version_check.outputs.VERSION_MISMATCH == 'true' + shell: pwsh + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add "*.csproj" + git commit -m "Update project versions to ${{ steps.version.outputs.VERSION }}" + git push origin main + + - name: Commit to build + id: commit + shell: pwsh + run: echo "SHA=$(git rev-parse HEAD)" >> $env:GITHUB_OUTPUT + + windows: + needs: prepare + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + ref: ${{ needs.prepare.outputs.sha }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore dependencies + run: dotnet restore CanonSDK.sln + + - name: Build solution + run: dotnet build CanonSDK.sln --configuration Release --no-restore + + - name: Run unit tests + run: dotnet test CanonSDK.sln --configuration Release --no-build + + - name: Publish the Windows package + run: dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0-windows --runtime win-x64 --self-contained true -p:PublishSingleFile=true --output ./publish + + - name: Check that the Windows package starts and loads the EDSDK + run: ./tools/Test-Package.ps1 -Executable ./publish/Canon.API.exe + shell: pwsh + + - name: Create the Windows archive + run: Compress-Archive -Path "./publish/*" -DestinationPath "./CanonWebAPI.zip" + shell: powershell + + - name: Upload the Windows archive + uses: actions/upload-artifact@v4 + with: + name: windows-package + path: CanonWebAPI.zip + if-no-files-found: error + + linux: + needs: prepare + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + ref: ${{ needs.prepare.outputs.sha }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Install libusb (needed by the Linux EDSDK) + run: sudo apt-get update && sudo apt-get install -y libusb-1.0-0 + + - name: Restore dependencies + run: dotnet restore CanonSDK.sln + + - name: Build solution + run: dotnet build CanonSDK.sln --configuration Release --no-restore + + - name: Run unit tests + run: dotnet test CanonSDK.sln --configuration Release --no-build + + - name: Publish the Linux package + run: dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0 --runtime linux-x64 --self-contained true -p:PublishSingleFile=true --output ./package/CanonWebAPI + + - name: Check that the Linux package starts and loads the EDSDK + run: ./tools/Test-Package.ps1 -Executable ./package/CanonWebAPI/Canon.API + shell: pwsh + + - name: Create the Linux archive + run: | + chmod 755 package/CanonWebAPI/Canon.API package/CanonWebAPI/install.sh + tar -czf CanonWebAPI-linux-x64.tar.gz -C package CanonWebAPI + + - name: Upload the Linux archive + uses: actions/upload-artifact@v4 + with: + name: linux-package + path: CanonWebAPI-linux-x64.tar.gz + if-no-files-found: error + + # Only when both packages were built and checked: a failure publishes nothing and leaves the photo booths alone. + release: + needs: [prepare, windows, linux] + if: github.event_name == 'push' + runs-on: ubuntu-latest + + steps: + - name: Checkout main + uses: actions/checkout@v5 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download the packages + uses: actions/download-artifact@v4 + with: + path: packages + merge-multiple: true + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + files: | + packages/CanonWebAPI.zip + packages/CanonWebAPI-linux-x64.tar.gz + fail_on_unmatched_files: true + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update AutoUpdater XML + shell: pwsh + run: | + $version = "${{ needs.prepare.outputs.version }}" + $releaseUrl = "https://github.com/${{ github.repository }}/releases/download/v$version/CanonWebAPI.zip" + + # Create docs folder if it doesn't exist + if (!(Test-Path "docs")) { + New-Item -ItemType Directory -Path "docs" + } + + # Create AutoUpdater XML content + $xmlContent = @" + + + $version + $releaseUrl + https://github.com/${{ github.repository }}/releases/tag/v$version + true + + "@ + + $xmlContent | Out-File -FilePath "docs/autoupdate.xml" -Encoding UTF8 + + - name: Commit AutoUpdater XML + shell: pwsh + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add docs/autoupdate.xml + if (!(git diff --staged --quiet)) { + git commit -m "Update AutoUpdater XML for version ${{ needs.prepare.outputs.version }}" + git push origin main + } +``` + +Check the syntax and the wiring: + +```bash +python3 - <<'EOF' +import yaml +w = yaml.safe_load(open('.github/workflows/build.yml')) +jobs = w['jobs'] +assert list(jobs) == ['prepare', 'windows', 'linux', 'release'], list(jobs) +assert jobs['release']['needs'] == ['prepare', 'windows', 'linux'] +assert jobs['windows']['needs'] == 'prepare' and jobs['linux']['needs'] == 'prepare' +print('build.yml OK, triggers:', list(w[True])) +EOF +``` + +Expected: `build.yml OK, triggers: ['push', 'workflow_dispatch']` (PyYAML reads the `on` key as `True`). + +- [ ] **Step 2: Documentation** + +`README.md`, "### Creating Releases": replace items 4 and 5 with: + +```markdown +4. **Packages**: The workflow builds and tests the solution, publishes the Windows and Linux packages and starts both to check that they load the EDSDK (`tools/Test-Package.ps1`) +5. **Atomic release**: Only when both packages succeeded, it creates the release (`CanonWebAPI.zip`, `CanonWebAPI-linux-x64.tar.gz`) and updates the AutoUpdater XML; a failure publishes nothing +``` + +and add after the list (before the `docs/autoupdate.xml` sentence): + +```markdown +To check the pipeline before tagging, run the "Release CanonWebAPI" workflow by hand from the Actions tab (on `main`): it builds, tests and checks both packages and publishes nothing. +``` + +`CLAUDE.md`, "## Release Process": replace the five bullets ("Trigger: ..." to "Updates AutoUpdater XML automatically") with: + +```markdown +- Trigger: push a tag matching `v*` on the latest commit of `main` (or run the workflow by hand: dry run, nothing published) +- `prepare` job: checks the tag, compares the tag version with the project versions and commits them if they differ +- `windows` and `linux` jobs, in parallel: build, test, publish `CanonWebAPI.zip` (win-x64) and `CanonWebAPI-linux-x64.tar.gz` (linux-x64, with `packaging/linux/`), and start each package with `tools/Test-Package.ps1` +- `release` job, only when both succeeded: creates the GitHub release with both packages and updates the AutoUpdater XML (Windows package) +``` + +`CHANGELOG.md`, `## [Unreleased]` / `### Changed`, add: + +```markdown +- The release workflow builds, tests and checks the Windows and Linux packages in parallel, and creates the release + (both packages) and updates the AutoUpdater XML only when both succeeded. It can be run by hand from the Actions tab + as a dry run that publishes nothing. +``` + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/build.yml README.md CLAUDE.md CHANGELOG.md +git commit -m "Build, check and release the Windows and Linux packages together + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 4: Pull request and CI (controller) + +**Files:** none. + +- [ ] **Step 1:** Push `feature/linux-package` and open a pull request to `main` (body ends with the Claude Code line). Wait for the CI: `build-and-test` (Windows) and `build-and-test-linux` must pass, including both package checks. +- [ ] **Step 2:** If the Linux package check fails on the runner with another `/status` error than a library load failure (spec, Risks), report it to the user before changing the expected message. + +--- + +### Task 5: Installation test with the camera (manual, with the user) + +**Files:** none (findings go to the user). + +- [ ] **Step 1: Build the archive exactly as the release job** + +```bash +S=/tmp/claude-1000/-home-gilbert-Documents-Project-CanonWebAPI/055083d5-ab07-4905-b425-34b4a1057fc9/scratchpad/linux-package +rm -rf "$S" && mkdir -p "$S/package" +dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0 --runtime linux-x64 --self-contained true -p:PublishSingleFile=true --output "$S/package/CanonWebAPI" +chmod 755 "$S/package/CanonWebAPI/Canon.API" "$S/package/CanonWebAPI/install.sh" +tar -czf "$S/CanonWebAPI-linux-x64.tar.gz" -C "$S/package" CanonWebAPI +mkdir "$S/extract" && tar -xzf "$S/CanonWebAPI-linux-x64.tar.gz" -C "$S/extract" && ls -l "$S/extract/CanonWebAPI" +``` + +- [ ] **Step 2: Install as a service.** Ask the user to plug the camera into this computer (not the VM) and to run `! sudo /tmp/claude-1000/-home-gilbert-Documents-Project-CanonWebAPI/055083d5-ab07-4905-b425-34b4a1057fc9/scratchpad/linux-package/extract/CanonWebAPI/install.sh`. Then check: + +```bash +systemctl status canonwebapi --no-pager +ls -l /opt/canonwebapi /opt/canonwebapi/logs +ls -l /dev/bus/usb/$(lsusb -d 04a9: | awk '{print $2 "/" substr($4, 1, 3)}') # group canonwebapi +curl -s http://localhost:5000/status +``` + +Expected: service active; files owned by root, `logs/` by `canonwebapi`; the camera device in group `canonwebapi`; `/status` connected. +- [ ] **Step 3: Camera test against the service:** + +```bash +/tmp/claude-1000/-home-gilbert-Documents-Project-CanonWebAPI/055083d5-ab07-4905-b425-34b4a1057fc9/scratchpad/pwsh/pwsh -NoProfile -File tools/Test-Camera.ps1 -BaseUrl http://localhost:5000 -NoBrowser +``` + +(captures with the live view, flash changes, power cycle). Then read `/opt/canonwebapi/logs/canon-api*.log` and `journalctl -u canonwebapi --no-pager | tail -50`. +- [ ] **Step 4: Update and uninstall.** Ask the user to run the `install.sh` of the extracted package again (update: service restarted, files replaced), then `! sudo /opt/canonwebapi/install.sh --uninstall`. Check that `systemctl status canonwebapi`, `/etc/udev/rules.d/60-canonwebapi.rules`, `/opt/canonwebapi` and `id canonwebapi` are all gone. + +--- + +### Task 6: Release 1.5.0.0 (controller, with the user) + +**Files:** +- Modify: `Canon.API/Canon.API.csproj`, `docs/openapi.json`, `README.md`, `CHANGELOG.md` + +- [ ] **Step 1: Version.** `Canon.API/Canon.API.csproj`: `AssemblyVersion`, `FileVersion` and `Version` to `1.5.0.0`. `docs/openapi.json`: `"version": "1.4.0"` to `"version": "1.5.0"`. README: "**Current version: 1.4.0.0**" to "**Current version: 1.5.0.0**", and the example tag in "Creating Releases" to `v1.5.0.0`. +- [ ] **Step 2: Upgrade note.** README, after the "Upgrading from 1.3.x" note: + +```markdown +> **Upgrading from 1.4.x**: the "flash firing" setting is no longer written while the live view runs (it froze the EOS R100): with `Canon:ForceFlashFiring` it is set when the camera connects, before the live view starts and before a capture taken without live view, and a value set with `POST /flash` stays until the live view stops. Linux is supported (see [Installing on Linux](#installing-on-linux)). From the sources, `dotnet run` and `dotnet publish` need `-f net10.0-windows` or `-f net10.0`. See the [changelog](CHANGELOG.md#1500---). +``` + +(`` is the release date in `YYYY-MM-DD` form, the same as in the changelog heading.) + +- [ ] **Step 3: Changelog.** `## [Unreleased]` becomes `## [1.5.0.0] - ` with a new empty `## [Unreleased]` above it; the links at the end: `[Unreleased]: .../compare/v1.5.0.0...HEAD` and `[1.5.0.0]: .../compare/v1.4.0.0...v1.5.0.0`. +- [ ] **Step 4:** Build and tests (`dotnet build CanonSDK.sln`, `dotnet test CanonSDK.sln`), commit "Bump version to 1.5.0.0 and date the 1.5.0.0 release", push, wait for the CI. +- [ ] **Step 5:** After the user merges the pull request: run the "Release CanonWebAPI" workflow by hand on `main` (dry run) and check that `windows` and `linux` pass. Then, only with the user's go-ahead, tag the latest commit of `main` with `v1.5.0.0` and push the tag; check the release assets and `docs/autoupdate.xml`. From 7133bf82dd3c2d2ecd752e0ca69ab1e9ed75cd35 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 19:44:52 -0400 Subject: [PATCH 03/16] Add the Linux package content and its systemd installer Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 ++ Canon.API/Canon.API.csproj | 11 ++++ README.md | 30 ++++++++++- packaging/linux/60-canonwebapi.rules | 2 + packaging/linux/canonwebapi.service | 14 +++++ packaging/linux/install.sh | 81 ++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 packaging/linux/60-canonwebapi.rules create mode 100644 packaging/linux/canonwebapi.service create mode 100755 packaging/linux/install.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 406670f..b6c7225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Versions match the Git tags (`v1.0.0.11`) that trigger the release workflow. ## [Unreleased] ### Added +- Linux package in every release: `CanonWebAPI-linux-x64.tar.gz`, a self-contained executable for Linux x64 with the + Linux EDSDK, and `install.sh`, which installs it as a systemd service with the USB access it needs + (`/opt/canonwebapi`, `canonwebapi` user, udev rule); `install.sh --uninstall` removes it. - `tools/Test-Camera.ps1` (with `Test-Camera.cmd`): runs the photo booth camera checks against the API (live view, captures, flash changes during the live view, power cycle) and saves the pictures, the results and the API log. - Linux x64 support: the `net10.0` target of `Canon.API` runs on Linux with the Linux version of the EDSDK diff --git a/Canon.API/Canon.API.csproj b/Canon.API/Canon.API.csproj index 6ecfb53..74c44d5 100644 --- a/Canon.API/Canon.API.csproj +++ b/Canon.API/Canon.API.csproj @@ -28,6 +28,12 @@ true + + + true + + @@ -80,6 +86,11 @@ + + + + + diff --git a/README.md b/README.md index 7cde9c1..b0af11b 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,32 @@ This application includes automatic update functionality powered by AutoUpdater. Linux has no automatic update yet (the `net10.0` target does not include AutoUpdater.NET). +## Installing on Linux + +Each release also provides `CanonWebAPI-linux-x64.tar.gz`: a self-contained executable for Linux x64 (no .NET to +install) with the Linux version of the EDSDK. Requirements: glibc 2.27 or later (e.g. Ubuntu 18.04, Debian 10 or +later), `libusb-1.0` (`sudo apt install libusb-1.0-0`), and systemd for the service. There is no automatic update on +Linux. + +```bash +tar -xzf CanonWebAPI-linux-x64.tar.gz +cd CanonWebAPI +./Canon.API # by hand: http://localhost:5000 +sudo ./install.sh # or as a service started at boot +``` + +`install.sh` copies the files to `/opt/canonwebapi`, creates the `canonwebapi` system user, adds a udev rule that lets +it open Canon cameras (USB vendor `04a9`), then installs, enables and starts the `canonwebapi` systemd service. + +* Settings: `/opt/canonwebapi/appsettings.Local.json` (kept by updates), then `sudo systemctl restart canonwebapi`. +* Logs: `/opt/canonwebapi/logs/` and `journalctl -u canonwebapi`; state: `systemctl status canonwebapi`. +* Update: extract the new archive and run its `install.sh`. +* Uninstall: `sudo /opt/canonwebapi/install.sh --uninstall` removes the service, the udev rule, `/opt/canonwebapi` + (settings and logs included) and the `canonwebapi` user. +* Run by hand, the API needs read/write access to the camera USB device: desktop sessions get it, otherwise install + the service. A desktop can mount the camera (GNOME: gvfs) and hold it: unmount it (`gio mount -l`, then + `gio mount -u `) or disable the automount. + ## Development & Building ### Building from Source @@ -228,7 +254,7 @@ dotnet run --project Canon.API -f net10.0-windows # on Linux: -f net10.0 ### Running on Linux The `net10.0` target of `Canon.API` runs on Linux x64 with the Linux version of the EDSDK (`EDSDK/linux-x64/libEDSDK.so`). -There is no automatic update and no release package for Linux yet: run it from the sources. +To run a release, see [Installing on Linux](#installing-on-linux); this section runs it from the sources. Prerequisites: .NET 10 SDK and `libusb-1.0` (Ubuntu: `sudo apt install dotnet-sdk-10.0 libusb-1.0-0`). @@ -306,7 +332,7 @@ update check out of the generator, which runs the application's entry point. * **Canon T7** ✅ ### System Requirements -* **OS**: Windows 10/11 (x64); Linux x64 from the sources (see [Running on Linux](#running-on-linux)) +* **OS**: Windows 10/11 (x64): `CanonWebAPI.zip`; Linux x64 (glibc 2.27+, `libusb-1.0`): `CanonWebAPI-linux-x64.tar.gz` (see [Installing on Linux](#installing-on-linux)) * **Runtime**: none to install: the release is a self-contained executable (.NET 10) * **Dependencies**: Canon EDSDK 13.20.21 64-bit libraries (included; up to 1.0.0.11: EDSDK 13.19.0) diff --git a/packaging/linux/60-canonwebapi.rules b/packaging/linux/60-canonwebapi.rules new file mode 100644 index 0000000..0a1b1bd --- /dev/null +++ b/packaging/linux/60-canonwebapi.rules @@ -0,0 +1,2 @@ +# CanonWebAPI: Canon cameras (USB vendor 04a9) can be opened by the canonwebapi service user. +SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="04a9", MODE="0660", GROUP="canonwebapi" diff --git a/packaging/linux/canonwebapi.service b/packaging/linux/canonwebapi.service new file mode 100644 index 0000000..be766d7 --- /dev/null +++ b/packaging/linux/canonwebapi.service @@ -0,0 +1,14 @@ +[Unit] +Description=CanonWebAPI (Canon camera web API) +After=network.target + +[Service] +User=canonwebapi +Group=canonwebapi +WorkingDirectory=/opt/canonwebapi +ExecStart=/opt/canonwebapi/Canon.API +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/packaging/linux/install.sh b/packaging/linux/install.sh new file mode 100755 index 0000000..56a159e --- /dev/null +++ b/packaging/linux/install.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Installs or updates CanonWebAPI as a systemd service from an extracted Linux package, or removes it. +# sudo ./install.sh install or update (appsettings.Local.json and the logs are kept) +# sudo ./install.sh --uninstall remove the service, the udev rule, /opt/canonwebapi and the canonwebapi user +set -euo pipefail + +readonly app_dir=/opt/canonwebapi +readonly service=canonwebapi +readonly user=canonwebapi +readonly unit_file=/etc/systemd/system/canonwebapi.service +readonly rule_file=/etc/udev/rules.d/60-canonwebapi.rules +package_dir="$(cd "$(dirname "$0")" && pwd)" +readonly package_dir + +fail() { + echo "Error: $*" >&2 + exit 1 +} + +uninstall() { + systemctl disable --now "$service" 2>/dev/null || true + rm -f "$unit_file" "$rule_file" + systemctl daemon-reload + udevadm control --reload + rm -rf "$app_dir" + if id "$user" >/dev/null 2>&1; then + userdel "$user" + fi + echo "CanonWebAPI removed." +} + +install_package() { + [ "$(uname -m)" = x86_64 ] || fail "this package is for x86_64, not $(uname -m)." + # No grep -q: with pipefail, grep exiting early would fail the pipeline through SIGPIPE. + ldconfig -p | grep 'libusb-1.0.so.0' >/dev/null || fail "libusb-1.0 is missing: sudo apt install libusb-1.0-0" + [ -f "$package_dir/Canon.API" ] || fail "Canon.API not found next to the script: run it from the extracted package." + [ "$package_dir" != "$app_dir" ] || fail "run the install.sh of the extracted package, not the installed one." + + if ! id "$user" >/dev/null 2>&1; then + useradd --system --user-group --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin "$user" + fi + + systemctl stop "$service" 2>/dev/null || true + + install -d -o root -g root -m 755 "$app_dir" + install -d -o "$user" -g "$user" -m 755 "$app_dir/logs" + + # Program files only: appsettings.Local.json and the logs of the installation are kept. + for file in "$package_dir"/*; do + name="$(basename "$file")" + case "$name" in + appsettings.Local.json | canonwebapi.service | 60-canonwebapi.rules) continue ;; + esac + [ -f "$file" ] || continue + install -o root -g root -m 644 "$file" "$app_dir/$name" + done + chmod 755 "$app_dir/Canon.API" "$app_dir/install.sh" + + install -o root -g root -m 644 "$package_dir/60-canonwebapi.rules" "$rule_file" + udevadm control --reload + # Applies the rule to a camera that is already plugged in. + udevadm trigger --subsystem-match=usb --attr-match=idVendor=04a9 + + install -o root -g root -m 644 "$package_dir/canonwebapi.service" "$unit_file" + systemctl daemon-reload + systemctl enable "$service" + systemctl restart "$service" + + echo "CanonWebAPI is installed in $app_dir and runs as the $service service: http://localhost:5000/status" + echo "Settings: $app_dir/appsettings.Local.json. Logs: $app_dir/logs and journalctl -u $service" + systemctl --no-pager status "$service" || true +} + +[ "$(id -u)" -eq 0 ] || fail "run it as root: sudo $0 $*" +command -v systemctl >/dev/null || fail "systemd is required." + +case "${1:-}" in + "") install_package ;; + --uninstall) uninstall ;; + *) fail "unknown option $1. Usage: sudo $0 [--uninstall]" ;; +esac From 408e1479e461468363d4df914d54e26cf28d3dbb Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 20:02:31 -0400 Subject: [PATCH 04/16] Fix the changelog entry that said there was no Linux package Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c7225..8c66408 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Versions match the Git tags (`v1.0.0.11`) that trigger the release workflow. - `tools/Test-Camera.ps1` (with `Test-Camera.cmd`): runs the photo booth camera checks against the API (live view, captures, flash changes during the live view, power cycle) and saves the pictures, the results and the API log. - Linux x64 support: the `net10.0` target of `Canon.API` runs on Linux with the Linux version of the EDSDK - (`EDSDK/linux-x64/libEDSDK.so`, needs `libusb-1.0`). No automatic update and no release package on Linux yet. + (`EDSDK/linux-x64/libEDSDK.so`, needs `libusb-1.0`). No automatic update on Linux. - Debug logs (`Logging:LogLevel:Canon.Core` = `Debug`): every EDSDK event, and each step of a capture (UI lock, flash settings, shutter button, transfer) with its result and duration. From 560ef183b4ee9609f201b4ff4d85b095b718a6b6 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 20:08:40 -0400 Subject: [PATCH 05/16] Check both packages in CI on Windows and Linux runners Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 47 +++++++++++++++++++++++++++++- CHANGELOG.md | 2 ++ CLAUDE.md | 1 + tools/Test-Package.ps1 | 62 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tools/Test-Package.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89e64ab..8e9e208 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,9 +35,54 @@ jobs: - name: Run unit tests run: dotnet test CanonSDK.sln --configuration Release --no-build --logger trx --results-directory TestResults + - name: Publish the Windows package + run: dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0-windows --runtime win-x64 --self-contained true -p:PublishSingleFile=true --output ./publish + + - name: Check that the Windows package starts and loads the EDSDK + run: ./tools/Test-Package.ps1 -Executable ./publish/Canon.API.exe + shell: pwsh + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-windows + path: TestResults + + build-and-test-linux: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Install libusb (needed by the Linux EDSDK) + run: sudo apt-get update && sudo apt-get install -y libusb-1.0-0 + + - name: Restore dependencies + run: dotnet restore CanonSDK.sln + + - name: Build solution + run: dotnet build CanonSDK.sln --configuration Release --no-restore + + - name: Run unit tests + run: dotnet test CanonSDK.sln --configuration Release --no-build --logger trx --results-directory TestResults + + - name: Publish the Linux package + run: dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0 --runtime linux-x64 --self-contained true -p:PublishSingleFile=true --output ./publish + + - name: Check that the Linux package starts and loads the EDSDK + run: ./tools/Test-Package.ps1 -Executable ./publish/Canon.API + shell: pwsh + - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: - name: test-results + name: test-results-linux path: TestResults diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c66408..0831d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ Versions match the Git tags (`v1.0.0.11`) that trigger the release workflow. - Regenerating `docs/openapi.json` at build time now uses `dotnet build Canon.API -f net10.0 -t:Build -t:GenerateOpenApiDocuments` (Windows or Linux); the former `-p:OpenApiGenerateDocumentsOnBuild=true` form generates nothing now that `Canon.API` has two target frameworks. +- CI builds and tests on Windows and Linux, then publishes the Windows and Linux packages and starts them to check that + they load the EDSDK (`tools/Test-Package.ps1`). The published Windows executable was never run before a release. ### Fixed - The camera froze when a picture was taken while the live view ran, as in the photo booth: the shutter stayed busy diff --git a/CLAUDE.md b/CLAUDE.md index 180b02a..2572ee0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,4 +133,5 @@ The project uses GitHub Actions for automated releases (record every change in t - **Canon.Core.Tests**: xUnit tests of the logic that does not need a camera (value tables, settings rules, timeouts, file types, CanonThread, LiveViewBroadcaster, EDSDK structure layouts) - **Canon.API.Tests**: xUnit tests of the error mapping (CameraExceptionHandler), the request models and the log levels +- CI (pull requests and `main`): build and tests on Windows and Linux runners, then both packages are published and started by `tools/Test-Package.ps1`, which checks that they load the EDSDK (`/status`: "No Canon camera detected") - End-to-end testing requires a physical Canon camera connected via USB (not possible in Windows containers) \ No newline at end of file diff --git a/tools/Test-Package.ps1 b/tools/Test-Package.ps1 new file mode 100644 index 0000000..fd14fd6 --- /dev/null +++ b/tools/Test-Package.ps1 @@ -0,0 +1,62 @@ +<# +.SYNOPSIS + Checks that a published CanonWebAPI package starts and loads the Canon EDSDK. + +.DESCRIPTION + Copies the folder of the executable to a temporary folder, so that its logs do not end up in the package, starts + it, polls GET /status and passes when the API answers with a connected camera or "No Canon camera detected" (no + camera on the machine). Any other answer, such as an EDSDK library that cannot be loaded, fails. Used by the CI and + release workflows on the Windows and Linux runners (PowerShell 7). + +.EXAMPLE + ./tools/Test-Package.ps1 -Executable ./publish/Canon.API.exe +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$Executable, + [string]$Url = 'http://localhost:5000', + [int]$TimeoutSeconds = 60 +) + +$ErrorActionPreference = 'Stop' + +$source = (Resolve-Path $Executable).Path +$work = Join-Path ([System.IO.Path]::GetTempPath()) ('canonwebapi-package-' + [guid]::NewGuid()) +Copy-Item (Split-Path $source) $work -Recurse +$exe = Join-Path $work (Split-Path $source -Leaf) +if ($IsLinux) { & chmod 755 $exe } +$output = Join-Path $work 'console.log' +$errorOutput = Join-Path $work 'console-error.log' + +$process = Start-Process $exe -WorkingDirectory $work -PassThru -RedirectStandardOutput $output ` + -RedirectStandardError $errorOutput -ArgumentList @("--urls=$Url", '--AutoUpdate:Enabled=false') +$status = $null + +try { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while (-not $status -and -not $process.HasExited -and (Get-Date) -lt $deadline) { + try { + $status = Invoke-RestMethod "$Url/status" -TimeoutSec 10 + } + catch { + Start-Sleep -Seconds 1 + } + } +} +finally { + if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force } + $process.WaitForExit() +} + +if ($status -and ($status.connected -or "$($status.error)" -match 'No Canon camera detected')) { + $detail = if ($status.connected) { "camera $($status.cameraName) connected" } else { $status.error } + Write-Host "Package OK: $detail" + Remove-Item $work -Recurse -Force + exit 0 +} + +$answer = if ($status) { $status | ConvertTo-Json -Compress } else { 'no answer' } +Write-Host "Package check failed. /status: $answer" +Write-Host '--- application output ---' +Get-Content $output, $errorOutput -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ } +exit 1 From 69974a70cceb026578f959dc361453031bda5c3a Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 20:29:56 -0400 Subject: [PATCH 06/16] Build, check and release the Windows and Linux packages together Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 203 +++++++++++++++++++++++++++--------- CHANGELOG.md | 3 + CLAUDE.md | 9 +- README.md | 6 +- 4 files changed, 166 insertions(+), 55 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a52355..7cbeb0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,25 +4,32 @@ on: push: tags: - 'v*' + # Dry run from the Actions tab: builds, tests and checks both packages of the selected branch; publishes nothing. + workflow_dispatch: permissions: contents: write jobs: - release: - runs-on: windows-latest - + prepare: + runs-on: ubuntu-latest + outputs: + sha: ${{ steps.commit.outputs.SHA }} + version: ${{ steps.version.outputs.VERSION }} + steps: - - name: Checkout main branch + # A release is built from main: this workflow commits the version and the AutoUpdater XML to it. + - name: Checkout uses: actions/checkout@v5 with: - ref: main + ref: ${{ github.event_name == 'push' && 'main' || github.ref }} fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - # The release is built from main (the workflow commits the version and AutoUpdater XML to it). - # Refuse to release if main is not the tagged commit, so the package always matches the tag. + # Refuse to release if main is not the tagged commit, so the packages always match the tag. - name: Check that the tag points to main + if: github.event_name == 'push' + shell: pwsh run: | $mainSha = git rev-parse HEAD $tagSha = "${{ github.sha }}" @@ -31,48 +38,49 @@ jobs: exit 1 } echo "Tag ${{ github.ref_name }} matches main ($mainSha)" - shell: powershell - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Extract version from tag + + - name: Get the version id: version + shell: pwsh run: | - $tagName = "${{ github.ref }}" -replace 'refs/tags/v', '' - echo "VERSION=$tagName" >> $env:GITHUB_OUTPUT - echo "Tag version: $tagName" - shell: powershell - + if ("${{ github.event_name }}" -eq "push") { + $version = "${{ github.ref_name }}" -replace '^v', '' + } + else { + $version = [regex]::Match((Get-Content Canon.API/Canon.API.csproj -Raw), '(.*)').Groups[1].Value + } + echo "VERSION=$version" >> $env:GITHUB_OUTPUT + echo "Version: $version" + - name: Check and update project version + if: github.event_name == 'push' id: version_check + shell: pwsh run: | $tagVersion = "${{ steps.version.outputs.VERSION }}" - + # Read current version from project file $projectFile = "Canon.API/Canon.API.csproj" $projectContent = Get-Content $projectFile $currentVersion = "" - + foreach ($line in $projectContent) { if ($line -match '(.*)') { $currentVersion = $matches[1] break } } - + echo "Current project version: $currentVersion" echo "Tag version: $tagVersion" - + if ($currentVersion -ne $tagVersion) { echo "VERSION_MISMATCH=true" >> $env:GITHUB_OUTPUT echo "Versions do not match - updating project files" - + # Update Canon.API project file $projectContent -replace '.*', "$tagVersion" -replace '.*', "$tagVersion" -replace '.*', "$tagVersion" | Set-Content $projectFile - + # Update Canon.Core project file if it has version info $coreProjectFile = "Canon.Core/Canon.Core.csproj" if (Test-Path $coreProjectFile) { @@ -85,53 +93,153 @@ jobs: echo "VERSION_MISMATCH=false" >> $env:GITHUB_OUTPUT echo "Versions match - no update needed" } - shell: powershell - + - name: Commit version updates - if: steps.version_check.outputs.VERSION_MISMATCH == 'true' + if: github.event_name == 'push' && steps.version_check.outputs.VERSION_MISMATCH == 'true' + shell: pwsh run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git add "*.csproj" git commit -m "Update project versions to ${{ steps.version.outputs.VERSION }}" git push origin main - shell: powershell - + + - name: Commit to build + id: commit + shell: pwsh + run: echo "SHA=$(git rev-parse HEAD)" >> $env:GITHUB_OUTPUT + + windows: + needs: prepare + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + ref: ${{ needs.prepare.outputs.sha }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Restore dependencies run: dotnet restore CanonSDK.sln - + - name: Build solution run: dotnet build CanonSDK.sln --configuration Release --no-restore - name: Run unit tests run: dotnet test CanonSDK.sln --configuration Release --no-build - - - name: Publish Canon.API as standalone executable + + - name: Publish the Windows package run: dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0-windows --runtime win-x64 --self-contained true -p:PublishSingleFile=true --output ./publish - - - name: Create release package - run: | - Compress-Archive -Path "./publish/*" -DestinationPath "./CanonWebAPI.zip" + + - name: Check that the Windows package starts and loads the EDSDK + run: ./tools/Test-Package.ps1 -Executable ./publish/Canon.API.exe + shell: pwsh + + - name: Create the Windows archive + run: Compress-Archive -Path "./publish/*" -DestinationPath "./CanonWebAPI.zip" shell: powershell + - name: Upload the Windows archive + uses: actions/upload-artifact@v4 + with: + name: windows-package + path: CanonWebAPI.zip + if-no-files-found: error + + linux: + needs: prepare + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + ref: ${{ needs.prepare.outputs.sha }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Install libusb (needed by the Linux EDSDK) + run: sudo apt-get update && sudo apt-get install -y libusb-1.0-0 + + - name: Restore dependencies + run: dotnet restore CanonSDK.sln + + - name: Build solution + run: dotnet build CanonSDK.sln --configuration Release --no-restore + + - name: Run unit tests + run: dotnet test CanonSDK.sln --configuration Release --no-build + + - name: Publish the Linux package + run: dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0 --runtime linux-x64 --self-contained true -p:PublishSingleFile=true --output ./package/CanonWebAPI + + - name: Check that the Linux package starts and loads the EDSDK + run: ./tools/Test-Package.ps1 -Executable ./package/CanonWebAPI/Canon.API + shell: pwsh + + - name: Create the Linux archive + run: | + chmod 755 package/CanonWebAPI/Canon.API package/CanonWebAPI/install.sh + tar -czf CanonWebAPI-linux-x64.tar.gz -C package CanonWebAPI + + - name: Upload the Linux archive + uses: actions/upload-artifact@v4 + with: + name: linux-package + path: CanonWebAPI-linux-x64.tar.gz + if-no-files-found: error + + # Only when both packages were built and checked: a failure publishes nothing and leaves the photo booths alone. + release: + needs: [prepare, windows, linux] + if: github.event_name == 'push' + runs-on: ubuntu-latest + + steps: + - name: Checkout main + uses: actions/checkout@v5 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download the packages + uses: actions/download-artifact@v4 + with: + path: packages + merge-multiple: true + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - files: ./CanonWebAPI.zip + tag_name: ${{ github.ref_name }} + files: | + packages/CanonWebAPI.zip + packages/CanonWebAPI-linux-x64.tar.gz + fail_on_unmatched_files: true generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + - name: Update AutoUpdater XML + shell: pwsh run: | - $version = "${{ steps.version.outputs.VERSION }}" + $version = "${{ needs.prepare.outputs.version }}" $releaseUrl = "https://github.com/${{ github.repository }}/releases/download/v$version/CanonWebAPI.zip" - + # Create docs folder if it doesn't exist if (!(Test-Path "docs")) { New-Item -ItemType Directory -Path "docs" } - + # Create AutoUpdater XML content $xmlContent = @" @@ -142,17 +250,16 @@ jobs: true "@ - + $xmlContent | Out-File -FilePath "docs/autoupdate.xml" -Encoding UTF8 - shell: powershell - + - name: Commit AutoUpdater XML + shell: pwsh run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git add docs/autoupdate.xml if (!(git diff --staged --quiet)) { - git commit -m "Update AutoUpdater XML for version ${{ steps.version.outputs.VERSION }}" + git commit -m "Update AutoUpdater XML for version ${{ needs.prepare.outputs.version }}" git push origin main } - shell: powershell \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0831d4f..2003898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ Versions match the Git tags (`v1.0.0.11`) that trigger the release workflow. `-p:OpenApiGenerateDocumentsOnBuild=true` form generates nothing now that `Canon.API` has two target frameworks. - CI builds and tests on Windows and Linux, then publishes the Windows and Linux packages and starts them to check that they load the EDSDK (`tools/Test-Package.ps1`). The published Windows executable was never run before a release. +- The release workflow builds, tests and checks the Windows and Linux packages in parallel, and creates the release + (both packages) and updates the AutoUpdater XML only when both succeeded. It can be run by hand from the Actions tab + as a dry run that publishes nothing. ### Fixed - The camera froze when a picture was taken while the live view ran, as in the photo booth: the shutter stayed busy diff --git a/CLAUDE.md b/CLAUDE.md index 2572ee0..f28e92f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,11 +110,10 @@ The application includes AutoUpdater.NET with these configurations: ## Release Process The project uses GitHub Actions for automated releases (record every change in the `Unreleased` section of `CHANGELOG.md`; never edit `docs/autoupdate.xml` by hand): -- Trigger: Push tags matching `v*` pattern -- Workflow compares tag version with project file versions -- Updates project files automatically if versions don't match -- Creates GitHub releases with packaged binaries -- Updates AutoUpdater XML automatically +- Trigger: push a tag matching `v*` on the latest commit of `main` (or run the workflow by hand: dry run, nothing published) +- `prepare` job: checks the tag, compares the tag version with the project versions and commits them if they differ +- `windows` and `linux` jobs, in parallel: build, test, publish `CanonWebAPI.zip` (win-x64) and `CanonWebAPI-linux-x64.tar.gz` (linux-x64, with `packaging/linux/`), and start each package with `tools/Test-Package.ps1` +- `release` job, only when both succeeded: creates the GitHub release with both packages and updates the AutoUpdater XML (Windows package) ## EDSDK Integration Notes diff --git a/README.md b/README.md index b0af11b..7d847fc 100644 --- a/README.md +++ b/README.md @@ -239,10 +239,12 @@ This project uses automated GitHub Actions for releases: 1. **Changelog**: Move the changes of the `Unreleased` section of [CHANGELOG.md](CHANGELOG.md) under the new version and date, and update the version in `Canon.API/Canon.API.csproj` 2. **Tag-based releases**: Push a tag like `v1.4.0.0` on the latest commit of `main` to trigger automated build and release (the workflow refuses a tag on another commit) 3. **Version synchronization**: The workflow automatically updates project versions to match the tag -4. **Unit tests**: The workflow runs `Canon.Core.Tests` and `Canon.API.Tests` before packaging -5. **Automatic packaging**: Creates release packages and updates the AutoUpdater XML +4. **Packages**: The workflow builds and tests the solution, publishes the Windows and Linux packages and starts both to check that they load the EDSDK (`tools/Test-Package.ps1`) +5. **Atomic release**: Only when both packages succeeded, it creates the release (`CanonWebAPI.zip`, `CanonWebAPI-linux-x64.tar.gz`) and updates the AutoUpdater XML; a failure publishes nothing 6. **GitHub releases**: Automatically creates GitHub releases with generated notes +To check the pipeline before tagging, run the "Release CanonWebAPI" workflow by hand from the Actions tab (on `main`): it builds, tests and checks both packages and publishes nothing. + `docs/autoupdate.xml` is updated by the workflow only: changing it by hand makes every installation download that version. ### Running the API From 0195eb2e0b092ed38f928875c9e1bad943a28fa2 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 21:19:58 -0400 Subject: [PATCH 07/16] Keep the camera usable by the Linux service after a replug 60-libgphoto2*.rules sorts after 60-canonwebapi.rules and, on add/bind events of a PTP device, sets MODE="0664", GROUP="plugdev": after a reboot, replug or camera power-on the service user could no longer open the camera. install.sh only sent a change event, which libgphoto2 skips, so it did not show right after the installation. The rule now uses final assignments and only matches Canon PTP cameras (printers and scanners are left alone). install.sh applies it with an add event and waits for udev before starting the service; --uninstall sends the same event so a plugged camera gets its default ownership back. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 1 + README.md | 3 ++- packaging/linux/60-canonwebapi.rules | 6 ++++-- packaging/linux/install.sh | 12 ++++++++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f28e92f..82d86e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,6 +31,7 @@ The solution consists of 4 projects: - Requires Canon EDSDK 13.20.21 **64-bit** DLLs (`EDSDK.dll`, `EdsImage.dll`, from `EDSDK_64/Dll` of the SDK) in the EDSDK folder - Linux: `EDSDK/linux-x64/libEDSDK.so` (from `EDSDK/Library/x86_64` of the 13.20.21 Linux SDK), needs `libusb-1.0` +- Linux package (`packaging/linux/`): the udev rule uses final assignments (`MODE:=`, `GROUP:=`) because `60-libgphoto2*.rules` sorts after it and sets `GROUP="plugdev"` on add/bind; `install.sh` applies it with an add event (libgphoto2 skips change events) - Uses Serilog for logging - Swagger/OpenAPI for API documentation - Platform target: x64 (Windows; Linux through the `net10.0` target of Canon.API) diff --git a/README.md b/README.md index 7d847fc..a098f4d 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,8 @@ sudo ./install.sh # or as a service started at boot ``` `install.sh` copies the files to `/opt/canonwebapi`, creates the `canonwebapi` system user, adds a udev rule that lets -it open Canon cameras (USB vendor `04a9`), then installs, enables and starts the `canonwebapi` systemd service. +it open Canon cameras (USB vendor `04a9`, PTP interface), then installs, enables and starts the `canonwebapi` systemd +service. * Settings: `/opt/canonwebapi/appsettings.Local.json` (kept by updates), then `sudo systemctl restart canonwebapi`. * Logs: `/opt/canonwebapi/logs/` and `journalctl -u canonwebapi`; state: `systemctl status canonwebapi`. diff --git a/packaging/linux/60-canonwebapi.rules b/packaging/linux/60-canonwebapi.rules index 0a1b1bd..ffdbc65 100644 --- a/packaging/linux/60-canonwebapi.rules +++ b/packaging/linux/60-canonwebapi.rules @@ -1,2 +1,4 @@ -# CanonWebAPI: Canon cameras (USB vendor 04a9) can be opened by the canonwebapi service user. -SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="04a9", MODE="0660", GROUP="canonwebapi" +# CanonWebAPI: Canon PTP cameras can be opened by the canonwebapi service user. +# Final assignments: 60-libgphoto2*.rules sorts after this file and sets MODE="0664", GROUP="plugdev" on add/bind. +SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="", IMPORT{builtin}="usb_id" +SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="04a9", ENV{ID_USB_INTERFACES}=="*:060101:*", MODE:="0660", GROUP:="canonwebapi" diff --git a/packaging/linux/install.sh b/packaging/linux/install.sh index 56a159e..16a0aa9 100755 --- a/packaging/linux/install.sh +++ b/packaging/linux/install.sh @@ -17,11 +17,20 @@ fail() { exit 1 } +# Applies the udev rules to a plugged camera as at boot or replug (60-libgphoto2*.rules skips change events), then +# waits until it has its final ownership. +apply_camera_rules() { + udevadm trigger --action=add --subsystem-match=usb --attr-match=idVendor=04a9 + udevadm settle +} + uninstall() { systemctl disable --now "$service" 2>/dev/null || true rm -f "$unit_file" "$rule_file" systemctl daemon-reload udevadm control --reload + # Gives a plugged camera its default ownership back. + apply_camera_rules rm -rf "$app_dir" if id "$user" >/dev/null 2>&1; then userdel "$user" @@ -58,8 +67,7 @@ install_package() { install -o root -g root -m 644 "$package_dir/60-canonwebapi.rules" "$rule_file" udevadm control --reload - # Applies the rule to a camera that is already plugged in. - udevadm trigger --subsystem-match=usb --attr-match=idVendor=04a9 + apply_camera_rules install -o root -g root -m 644 "$package_dir/canonwebapi.service" "$unit_file" systemctl daemon-reload From 6a88ccf3e184bb6d9de39a1489b08ea0ebae854e Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 21:22:18 -0400 Subject: [PATCH 08/16] Check the system and the package before install.sh changes anything install.sh only checked Canon.API up front: a missing libEDSDK.so, unit or udev rule was found after the service had been stopped. It now checks root, x86_64, that systemd runs as init (not only that systemctl exists), libusb and every package file before the first change; --uninstall needs root and systemd only. After the restart it waits a few seconds and fails when the service is not running, pointing to journalctl, instead of reporting a successful installation of a service that crash-loops. Also: the comment of the copy loop says what it leaves alone (appsettings.Local.json and logs/), and the root check message no longer ends with a space when there is no argument. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 ++- packaging/linux/install.sh | 15 +++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a098f4d..a6454b4 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,8 @@ sudo ./install.sh # or as a service started at boot `install.sh` copies the files to `/opt/canonwebapi`, creates the `canonwebapi` system user, adds a udev rule that lets it open Canon cameras (USB vendor `04a9`, PTP interface), then installs, enables and starts the `canonwebapi` systemd -service. +service. It checks the system and the package before changing anything, and fails when the service is not running a +few seconds after its start (see `journalctl -u canonwebapi`). * Settings: `/opt/canonwebapi/appsettings.Local.json` (kept by updates), then `sudo systemctl restart canonwebapi`. * Logs: `/opt/canonwebapi/logs/` and `journalctl -u canonwebapi`; state: `systemctl status canonwebapi`. diff --git a/packaging/linux/install.sh b/packaging/linux/install.sh index 16a0aa9..4e4fd11 100755 --- a/packaging/linux/install.sh +++ b/packaging/linux/install.sh @@ -39,11 +39,15 @@ uninstall() { } install_package() { + # Every check comes before the first change, so a failed check leaves the system as it was. [ "$(uname -m)" = x86_64 ] || fail "this package is for x86_64, not $(uname -m)." # No grep -q: with pipefail, grep exiting early would fail the pipeline through SIGPIPE. ldconfig -p | grep 'libusb-1.0.so.0' >/dev/null || fail "libusb-1.0 is missing: sudo apt install libusb-1.0-0" - [ -f "$package_dir/Canon.API" ] || fail "Canon.API not found next to the script: run it from the extracted package." [ "$package_dir" != "$app_dir" ] || fail "run the install.sh of the extracted package, not the installed one." + local file + for file in Canon.API libEDSDK.so canonwebapi.service 60-canonwebapi.rules; do + [ -f "$package_dir/$file" ] || fail "$file not found next to the script: run it from the extracted package." + done if ! id "$user" >/dev/null 2>&1; then useradd --system --user-group --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin "$user" @@ -54,7 +58,7 @@ install_package() { install -d -o root -g root -m 755 "$app_dir" install -d -o "$user" -g "$user" -m 755 "$app_dir/logs" - # Program files only: appsettings.Local.json and the logs of the installation are kept. + # The appsettings.Local.json and logs/ of the installation are left as they are. for file in "$package_dir"/*; do name="$(basename "$file")" case "$name" in @@ -73,14 +77,17 @@ install_package() { systemctl daemon-reload systemctl enable "$service" systemctl restart "$service" + # A startup failure (port 5000 in use, invalid appsettings.Local.json...) ends the process within a few seconds. + sleep 3 + systemctl is-active --quiet "$service" || fail "the $service service is not running: see journalctl -u $service" echo "CanonWebAPI is installed in $app_dir and runs as the $service service: http://localhost:5000/status" echo "Settings: $app_dir/appsettings.Local.json. Logs: $app_dir/logs and journalctl -u $service" systemctl --no-pager status "$service" || true } -[ "$(id -u)" -eq 0 ] || fail "run it as root: sudo $0 $*" -command -v systemctl >/dev/null || fail "systemd is required." +[ "$(id -u)" -eq 0 ] || fail "run it as root: sudo $0${*:+ $*}" +[ -d /run/systemd/system ] || fail "systemd is not running: the $service service needs it." case "${1:-}" in "") install_package ;; From 4a4f9413dd09f659a09f6d8123fd1f8887b30a35 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 21:22:38 -0400 Subject: [PATCH 09/16] Harden the Linux systemd service NoNewPrivileges, PrivateTmp, ProtectSystem=full (/opt, where the logs are written, stays writable) and ProtectHome. PrivateDevices would hide the camera, MemoryDenyWriteExecute breaks the JIT and an address family restriction would block the netlink socket used for hot plug: the unit says so, so they are not added later by mistake. Co-Authored-By: Claude Opus 5.5 --- packaging/linux/canonwebapi.service | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packaging/linux/canonwebapi.service b/packaging/linux/canonwebapi.service index be766d7..8cd6ae6 100644 --- a/packaging/linux/canonwebapi.service +++ b/packaging/linux/canonwebapi.service @@ -9,6 +9,11 @@ WorkingDirectory=/opt/canonwebapi ExecStart=/opt/canonwebapi/Canon.API Restart=on-failure RestartSec=5 +# No PrivateDevices (USB camera), MemoryDenyWriteExecute (JIT) or RestrictAddressFamilies (hot plug uses netlink). +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=full +ProtectHome=yes [Install] WantedBy=multi-user.target From d4b0c6c7337a8e085f94ba4c7e436d6c8dccd649 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 21:26:13 -0400 Subject: [PATCH 10/16] Run the Linux package without ICU The single file loaded libicu at runtime: on a minimal system the service aborted with "Couldn't find a valid ICU package" and crash-looped. The API only uses the invariant culture, so the net10.0 target now uses invariant globalization. Checked with an ICU that cannot be found (DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU=99.9): the previous build aborts, this one answers /status. The comment of the property group no longer reads as if it made the publish self-contained: the publish command line does. README lists the actual requirements (glibc 2.27, libstdc++ of GCC 7, libusb, systemd; no ICU) and the tested distribution instead of untested examples. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++--- CLAUDE.md | 2 +- Canon.API/Canon.API.csproj | 6 ++++-- README.md | 6 +++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2003898..f95140d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,9 @@ Versions match the Git tags (`v1.0.0.11`) that trigger the release workflow. ## [Unreleased] ### Added -- Linux package in every release: `CanonWebAPI-linux-x64.tar.gz`, a self-contained executable for Linux x64 with the - Linux EDSDK, and `install.sh`, which installs it as a systemd service with the USB access it needs - (`/opt/canonwebapi`, `canonwebapi` user, udev rule); `install.sh --uninstall` removes it. +- Linux package in every release: `CanonWebAPI-linux-x64.tar.gz`, a self-contained executable for Linux x64 (no .NET + or ICU to install) with the Linux EDSDK, and `install.sh`, which installs it as a systemd service with the USB + access it needs (`/opt/canonwebapi`, `canonwebapi` user, udev rule); `install.sh --uninstall` removes it. - `tools/Test-Camera.ps1` (with `Test-Camera.cmd`): runs the photo booth camera checks against the API (live view, captures, flash changes during the live view, power cycle) and saves the pictures, the results and the API log. - Linux x64 support: the `net10.0` target of `Canon.API` runs on Linux with the Linux version of the EDSDK diff --git a/CLAUDE.md b/CLAUDE.md index 82d86e6..2b24fc3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ The solution consists of 4 projects: ### Dependencies - Requires Canon EDSDK 13.20.21 **64-bit** DLLs (`EDSDK.dll`, `EdsImage.dll`, from `EDSDK_64/Dll` of the SDK) in the EDSDK folder -- Linux: `EDSDK/linux-x64/libEDSDK.so` (from `EDSDK/Library/x86_64` of the 13.20.21 Linux SDK), needs `libusb-1.0` +- Linux: `EDSDK/linux-x64/libEDSDK.so` (from `EDSDK/Library/x86_64` of the 13.20.21 Linux SDK), needs `libusb-1.0`; the `net10.0` target uses invariant globalization (no ICU needed: only the invariant culture is used) - Linux package (`packaging/linux/`): the udev rule uses final assignments (`MODE:=`, `GROUP:=`) because `60-libgphoto2*.rules` sorts after it and sets `GROUP="plugdev"` on add/bind; `install.sh` applies it with an add event (libgphoto2 skips change events) - Uses Serilog for logging - Swagger/OpenAPI for API documentation diff --git a/Canon.API/Canon.API.csproj b/Canon.API/Canon.API.csproj index 74c44d5..2d826f0 100644 --- a/Canon.API/Canon.API.csproj +++ b/Canon.API/Canon.API.csproj @@ -28,10 +28,12 @@ true - + true + true diff --git a/README.md b/README.md index a6454b4..45c7828 100644 --- a/README.md +++ b/README.md @@ -190,9 +190,9 @@ Linux has no automatic update yet (the `net10.0` target does not include AutoUpd ## Installing on Linux Each release also provides `CanonWebAPI-linux-x64.tar.gz`: a self-contained executable for Linux x64 (no .NET to -install) with the Linux version of the EDSDK. Requirements: glibc 2.27 or later (e.g. Ubuntu 18.04, Debian 10 or -later), `libusb-1.0` (`sudo apt install libusb-1.0-0`), and systemd for the service. There is no automatic update on -Linux. +install) with the Linux version of the EDSDK. Requirements: x86_64 Linux with glibc 2.27 or later and libstdc++ (GCC 7 +or later), `libusb-1.0` (`sudo apt install libusb-1.0-0`), and systemd for the service; no ICU library is needed. +Tested on Ubuntu 26.04. There is no automatic update on Linux. ```bash tar -xzf CanonWebAPI-linux-x64.tar.gz From 98dce1b3aae48c9460769dc7dfd0f5d2faab1b64 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 21:27:22 -0400 Subject: [PATCH 11/16] Exit with code 1 on a fatal startup error The catch around startup only logged the error, so the process exited with code 0 when the port was in use or appsettings.Local.json was invalid: launchers did not see the failure and systemd's Restart=on-failure never fired. Checked on the published Linux package with port 5097 held by another process: exit code 0 before, 1 now; a normal stop (SIGTERM) still exits with 0. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ Canon.API/Program.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f95140d..30a957e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ Versions match the Git tags (`v1.0.0.11`) that trigger the release workflow. - `Canon.API` has two target frameworks: `dotnet run` and `dotnet publish` need `-f net10.0-windows` (Windows) or `-f net10.0` (Linux). `Canon.Core` and the unit tests target `net10.0`. - A failed UI unlock after setting the flash is logged as a warning; it was ignored. +- A fatal startup error (e.g. port already in use, invalid `appsettings.Local.json`) now exits with code 1. It exited + with code 0, so launchers and systemd did not see the failure. - The problem title returned when the Canon EDSDK cannot be loaded now names both platforms: "Canon EDSDK could not be loaded (EDSDK.dll or libEDSDK.so missing, wrong architecture, or libusb-1.0 missing on Linux)" (was "...(EDSDK.dll missing or wrong architecture)"). Client-visible text. diff --git a/Canon.API/Program.cs b/Canon.API/Program.cs index 24ab7db..5b4ebbe 100644 --- a/Canon.API/Program.cs +++ b/Canon.API/Program.cs @@ -75,6 +75,8 @@ catch (Exception ex) when (ex is not HostAbortedException) { Log.Fatal(ex, "Application startup failed"); + // Launchers and systemd (Restart=on-failure) only see the failure through a non-zero exit code. + Environment.ExitCode = 1; } finally { From 4f92a855c1e2a7226addc815e6703c97b7430446 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 21:28:00 -0400 Subject: [PATCH 12/16] Give the release build jobs a read-only token The windows and linux jobs run NuGet build logic, the tests and the package checks with a persisted contents: write token. The workflow now defaults to contents: read, only prepare (version commit) and release (GitHub release, AutoUpdater XML) get contents: write, and the build jobs check out without persisting credentials. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 9 ++++++++- CHANGELOG.md | 3 ++- CLAUDE.md | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7cbeb0b..dc678fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,12 +7,15 @@ on: # Dry run from the Actions tab: builds, tests and checks both packages of the selected branch; publishes nothing. workflow_dispatch: +# Read-only by default: the build jobs run NuGet build logic and the tests. Only prepare and release push or publish. permissions: - contents: write + contents: read jobs: prepare: runs-on: ubuntu-latest + permissions: + contents: write outputs: sha: ${{ steps.commit.outputs.SHA }} version: ${{ steps.version.outputs.VERSION }} @@ -118,6 +121,7 @@ jobs: uses: actions/checkout@v5 with: ref: ${{ needs.prepare.outputs.sha }} + persist-credentials: false - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -160,6 +164,7 @@ jobs: uses: actions/checkout@v5 with: ref: ${{ needs.prepare.outputs.sha }} + persist-credentials: false - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -202,6 +207,8 @@ jobs: needs: [prepare, windows, linux] if: github.event_name == 'push' runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout main diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a957e..db93108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,8 @@ Versions match the Git tags (`v1.0.0.11`) that trigger the release workflow. they load the EDSDK (`tools/Test-Package.ps1`). The published Windows executable was never run before a release. - The release workflow builds, tests and checks the Windows and Linux packages in parallel, and creates the release (both packages) and updates the AutoUpdater XML only when both succeeded. It can be run by hand from the Actions tab - as a dry run that publishes nothing. + as a dry run that publishes nothing. The build jobs get a read-only token: only the jobs that push or publish can + write to the repository. ### Fixed - The camera froze when a picture was taken while the live view ran, as in the photo booth: the shutter stayed busy diff --git a/CLAUDE.md b/CLAUDE.md index 2b24fc3..5b1adaa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,6 +115,7 @@ The project uses GitHub Actions for automated releases (record every change in t - `prepare` job: checks the tag, compares the tag version with the project versions and commits them if they differ - `windows` and `linux` jobs, in parallel: build, test, publish `CanonWebAPI.zip` (win-x64) and `CanonWebAPI-linux-x64.tar.gz` (linux-x64, with `packaging/linux/`), and start each package with `tools/Test-Package.ps1` - `release` job, only when both succeeded: creates the GitHub release with both packages and updates the AutoUpdater XML (Windows package) +- Token scope: `contents: read` for the workflow and no persisted credentials in the build jobs (they run NuGet build logic and the tests); only `prepare` and `release` get `contents: write` ## EDSDK Integration Notes From f9a47537e8713a66033d0b8048e91331ed06eee8 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 21:30:24 -0400 Subject: [PATCH 13/16] Dry-run the release job and polish the release workflow - The release job also runs on workflow_dispatch: it checks out the selected branch, downloads both packages, lists them and generates the AutoUpdater XML (printed); only "Create GitHub Release" and "Commit AutoUpdater XML" are limited to tag pushes. A dry run now covers the download and the XML generation, and still publishes nothing. - The AutoUpdater XML is written with a UTF-8 BOM, as Windows PowerShell always wrote it: the feed stays byte identical (pwsh on Linux dropped the BOM). - The Linux archive records root (0/0) as owner: extracted with sudo, its files belonged to the runner's uid, whose local user could edit install.sh before root runs it. - 30 minute timeout on the build jobs of both workflows (a hung check held a runner for 6 hours), and no comments that restate the code. README states what a failure leaves behind: a failed build job publishes nothing; a release job that fails after creating the release leaves the feed (and the photo booths) on the previous version until it is rerun. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 31 +++++++++++++++++++------------ .github/workflows/ci.yml | 2 ++ CHANGELOG.md | 5 +++-- CLAUDE.md | 2 +- README.md | 4 ++-- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc678fa..125a9f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,8 @@ on: push: tags: - 'v*' - # Dry run from the Actions tab: builds, tests and checks both packages of the selected branch; publishes nothing. + # Dry run from the Actions tab: builds, tests and checks both packages of the selected branch, downloads them in the + # release job and generates the AutoUpdater XML; creates no release and pushes nothing. workflow_dispatch: # Read-only by default: the build jobs run NuGet build logic and the tests. Only prepare and release push or publish. @@ -62,7 +63,6 @@ jobs: run: | $tagVersion = "${{ steps.version.outputs.VERSION }}" - # Read current version from project file $projectFile = "Canon.API/Canon.API.csproj" $projectContent = Get-Content $projectFile $currentVersion = "" @@ -81,10 +81,8 @@ jobs: echo "VERSION_MISMATCH=true" >> $env:GITHUB_OUTPUT echo "Versions do not match - updating project files" - # Update Canon.API project file $projectContent -replace '.*', "$tagVersion" -replace '.*', "$tagVersion" -replace '.*', "$tagVersion" | Set-Content $projectFile - # Update Canon.Core project file if it has version info $coreProjectFile = "Canon.Core/Canon.Core.csproj" if (Test-Path $coreProjectFile) { $coreContent = Get-Content $coreProjectFile @@ -115,6 +113,7 @@ jobs: windows: needs: prepare runs-on: windows-latest + timeout-minutes: 30 steps: - name: Checkout @@ -158,6 +157,7 @@ jobs: linux: needs: prepare runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Checkout @@ -190,10 +190,12 @@ jobs: run: ./tools/Test-Package.ps1 -Executable ./package/CanonWebAPI/Canon.API shell: pwsh + # Owned by root when extracted with sudo: with the runner's uid, the local user with that uid could edit install.sh + # before root runs it. - name: Create the Linux archive run: | chmod 755 package/CanonWebAPI/Canon.API package/CanonWebAPI/install.sh - tar -czf CanonWebAPI-linux-x64.tar.gz -C package CanonWebAPI + tar --owner=0 --group=0 --numeric-owner -czf CanonWebAPI-linux-x64.tar.gz -C package CanonWebAPI - name: Upload the Linux archive uses: actions/upload-artifact@v4 @@ -202,19 +204,18 @@ jobs: path: CanonWebAPI-linux-x64.tar.gz if-no-files-found: error - # Only when both packages were built and checked: a failure publishes nothing and leaves the photo booths alone. + # Runs only when both packages were built and checked. A dry run stops before publishing: no release, no push. release: needs: [prepare, windows, linux] - if: github.event_name == 'push' runs-on: ubuntu-latest permissions: contents: write steps: - - name: Checkout main + - name: Checkout uses: actions/checkout@v5 with: - ref: main + ref: ${{ github.event_name == 'push' && 'main' || github.ref }} fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} @@ -224,7 +225,12 @@ jobs: path: packages merge-multiple: true + - name: List the packages + shell: pwsh + run: Get-ChildItem packages + - name: Create GitHub Release + if: github.event_name == 'push' uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} @@ -242,12 +248,10 @@ jobs: $version = "${{ needs.prepare.outputs.version }}" $releaseUrl = "https://github.com/${{ github.repository }}/releases/download/v$version/CanonWebAPI.zip" - # Create docs folder if it doesn't exist if (!(Test-Path "docs")) { New-Item -ItemType Directory -Path "docs" } - # Create AutoUpdater XML content $xmlContent = @" @@ -258,9 +262,12 @@ jobs: "@ - $xmlContent | Out-File -FilePath "docs/autoupdate.xml" -Encoding UTF8 + # The feed has always started with a BOM (written by Windows PowerShell); pwsh's UTF8 would drop it. + $xmlContent | Out-File -FilePath "docs/autoupdate.xml" -Encoding utf8BOM + Get-Content docs/autoupdate.xml - name: Commit AutoUpdater XML + if: github.event_name == 'push' shell: pwsh run: | git config --local user.email "action@github.com" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e9e208..c816898 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ concurrency: jobs: build-and-test: runs-on: windows-latest + timeout-minutes: 30 steps: - name: Checkout @@ -51,6 +52,7 @@ jobs: build-and-test-linux: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index db93108..72a1a02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,8 +34,9 @@ Versions match the Git tags (`v1.0.0.11`) that trigger the release workflow. they load the EDSDK (`tools/Test-Package.ps1`). The published Windows executable was never run before a release. - The release workflow builds, tests and checks the Windows and Linux packages in parallel, and creates the release (both packages) and updates the AutoUpdater XML only when both succeeded. It can be run by hand from the Actions tab - as a dry run that publishes nothing. The build jobs get a read-only token: only the jobs that push or publish can - write to the repository. + as a dry run: it also downloads both packages and generates the AutoUpdater XML in the release job, but creates no + release and pushes nothing. The build jobs get a read-only token: only the jobs that push or publish can write to + the repository. ### Fixed - The camera froze when a picture was taken while the live view ran, as in the photo booth: the shutter stayed busy diff --git a/CLAUDE.md b/CLAUDE.md index 5b1adaa..4d42f43 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,7 +114,7 @@ The project uses GitHub Actions for automated releases (record every change in t - Trigger: push a tag matching `v*` on the latest commit of `main` (or run the workflow by hand: dry run, nothing published) - `prepare` job: checks the tag, compares the tag version with the project versions and commits them if they differ - `windows` and `linux` jobs, in parallel: build, test, publish `CanonWebAPI.zip` (win-x64) and `CanonWebAPI-linux-x64.tar.gz` (linux-x64, with `packaging/linux/`), and start each package with `tools/Test-Package.ps1` -- `release` job, only when both succeeded: creates the GitHub release with both packages and updates the AutoUpdater XML (Windows package) +- `release` job, only when both succeeded: creates the GitHub release with both packages and updates the AutoUpdater XML (Windows package, written with a UTF-8 BOM as it always was); a dry run only downloads the packages and generates the XML - Token scope: `contents: read` for the workflow and no persisted credentials in the build jobs (they run NuGet build logic and the tests); only `prepare` and `release` get `contents: write` ## EDSDK Integration Notes diff --git a/README.md b/README.md index 45c7828..9672f58 100644 --- a/README.md +++ b/README.md @@ -242,10 +242,10 @@ This project uses automated GitHub Actions for releases: 2. **Tag-based releases**: Push a tag like `v1.4.0.0` on the latest commit of `main` to trigger automated build and release (the workflow refuses a tag on another commit) 3. **Version synchronization**: The workflow automatically updates project versions to match the tag 4. **Packages**: The workflow builds and tests the solution, publishes the Windows and Linux packages and starts both to check that they load the EDSDK (`tools/Test-Package.ps1`) -5. **Atomic release**: Only when both packages succeeded, it creates the release (`CanonWebAPI.zip`, `CanonWebAPI-linux-x64.tar.gz`) and updates the AutoUpdater XML; a failure publishes nothing +5. **Atomic release**: Only when both packages succeeded, it creates the release (`CanonWebAPI.zip`, `CanonWebAPI-linux-x64.tar.gz`), then updates the AutoUpdater XML. A failure of the build jobs publishes nothing; if the release job fails after creating the release, the feed is not updated and the photo booths stay on the previous version: rerun the failed job 6. **GitHub releases**: Automatically creates GitHub releases with generated notes -To check the pipeline before tagging, run the "Release CanonWebAPI" workflow by hand from the Actions tab (on `main`): it builds, tests and checks both packages and publishes nothing. +To check the pipeline before tagging, run the "Release CanonWebAPI" workflow by hand from the Actions tab (on `main`): it builds, tests and checks both packages, downloads them in the release job and generates the AutoUpdater XML, without creating a release or pushing anything. `docs/autoupdate.xml` is updated by the workflow only: changing it by hand makes every installation download that version. From 796424a25cf3b4d6982111718e316a36b4ff0e57 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 21:30:53 -0400 Subject: [PATCH 14/16] Fix the Linux and release documentation README: stop the service before running Canon.API by hand (one program at a time can use the camera, and the service also listens on port 5000). CLAUDE.md: the release no longer publishes only -f net10.0-windows; it publishes the Windows package with -f net10.0-windows -r win-x64 and the Linux package with -f net10.0 -r linux-x64. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- README.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4d42f43..3f795cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,7 +90,7 @@ Errors are problem details: 400 invalid value, 409 capture refused or setting lo - The camera can be connected via USB before or after the application starts (automatic connection and reconnection) - Canon EOS Utility must NOT be running (conflicts with EDSDK access) - Requires compatible Canon camera with EDSDK support -- Canon.Core and the tests target `net10.0`; Canon.API targets `net10.0-windows` (release, AutoUpdater.NET) and `net10.0` (Linux). Publish the release with `-f net10.0-windows` +- Canon.Core and the tests target `net10.0`; Canon.API targets `net10.0-windows` (Windows release, AutoUpdater.NET) and `net10.0` (Linux). The release publishes the Windows package with `-f net10.0-windows -r win-x64` and the Linux package with `-f net10.0 -r linux-x64` (both self-contained single files) - Uses structured logging with Serilog (logs to console and `logs/canon-api.log` next to the executable; levels from `Logging:LogLevel`) - Content root is the executable folder: settings files are read next to the executable whatever the working directory - Operator settings go in `appsettings.Local.json`, never shipped: automatic updates overwrite `appsettings.json` diff --git a/README.md b/README.md index 9672f58..15463de 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,8 @@ few seconds after its start (see `journalctl -u canonwebapi`). * Run by hand, the API needs read/write access to the camera USB device: desktop sessions get it, otherwise install the service. A desktop can mount the camera (GNOME: gvfs) and hold it: unmount it (`gio mount -l`, then `gio mount -u `) or disable the automount. +* Stop the service before running `Canon.API` by hand (`sudo systemctl stop canonwebapi`): only one program can use + the camera, and the service also uses port 5000. ## Development & Building From 5e57d58d2b347b35f596f6e177dd3cfebf4ddf13 Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 21:48:18 -0400 Subject: [PATCH 15/16] Bring the Linux package design up to date with the final review fixes The design still described the udev rule, the install.sh checks, the service unit and the release dry run as they were before the fixes, and the README requirements summary did not mention libstdc++. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- specs/2026-10-04-linux-package-design.md | 55 +++++++++++++++++------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 15463de..ea2f265 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,7 @@ update check out of the generator, which runs the application's entry point. * **Canon T7** ✅ ### System Requirements -* **OS**: Windows 10/11 (x64): `CanonWebAPI.zip`; Linux x64 (glibc 2.27+, `libusb-1.0`): `CanonWebAPI-linux-x64.tar.gz` (see [Installing on Linux](#installing-on-linux)) +* **OS**: Windows 10/11 (x64): `CanonWebAPI.zip`; Linux x64 (glibc 2.27+, libstdc++ from GCC 7+, `libusb-1.0`): `CanonWebAPI-linux-x64.tar.gz` (see [Installing on Linux](#installing-on-linux)) * **Runtime**: none to install: the release is a self-contained executable (.NET 10) * **Dependencies**: Canon EDSDK 13.20.21 64-bit libraries (included; up to 1.0.0.11: EDSDK 13.19.0) diff --git a/specs/2026-10-04-linux-package-design.md b/specs/2026-10-04-linux-package-design.md index 9c27d47..34da910 100644 --- a/specs/2026-10-04-linux-package-design.md +++ b/specs/2026-10-04-linux-package-design.md @@ -1,6 +1,7 @@ # Linux support, step 2: a Linux package in release 1.5.0.0 -Date: 2026-10-04. Status: approved design. +Date: 2026-10-04. Status: approved design, amended after the final review (udev rule with final assignments, install.sh +checks, service hardening, invariant globalization, release token scope and dry run). ## Context @@ -42,7 +43,8 @@ ARM (arm64, arm32), automatic update on Linux, `.deb` or other distribution pack Windows package); - `install.sh`, `canonwebapi.service`, `60-canonwebapi.rules` (section 2). - Built with `dotnet publish Canon.API/Canon.API.csproj -c Release -f net10.0 -r linux-x64 --self-contained true - -p:PublishSingleFile=true`. `EnableCompressionInSingleFile` applies to Linux as to Windows. No + -p:PublishSingleFile=true`. `EnableCompressionInSingleFile` applies to Linux as to Windows, and the `net10.0` target + uses `InvariantGlobalization`: the API only formats with the invariant culture, so no ICU library is needed. No `IncludeNativeLibrariesForSelfExtract`: nothing would be extracted (verified above), and a service user has no home directory to extract to. - The package content is defined in `Canon.API.csproj`, like the Windows DLLs: for the `net10.0` target, the files of @@ -60,17 +62,22 @@ removes. Files in `packaging/linux/`: `install.sh`, `canonwebapi.service`, `60-c Install or update: -1. Checks: root, `x86_64`, systemd present, `libusb-1.0.so.0` known to `ldconfig`. When libusb is missing, prints - `sudo apt install libusb-1.0-0` and stops (no package installed by the script). +1. Checks, before anything changes: root, systemd running as init (`/run/systemd/system`), `x86_64`, + `libusb-1.0.so.0` known to `ldconfig`, and the package files next to the script (`Canon.API`, `libEDSDK.so`, + `canonwebapi.service`, `60-canonwebapi.rules`; the installed copy in `/opt/canonwebapi` only uninstalls). When + libusb is missing, prints `sudo apt install libusb-1.0-0` and stops (no package installed by the script). 2. Creates the system user `canonwebapi` when missing: no login shell, no home directory. 3. Stops the service when it runs (update). 4. Copies the program files to `/opt/canonwebapi`, owned by root. `logs/` is owned by `canonwebapi`. An existing `appsettings.Local.json` is never overwritten or removed; none is created. -5. Installs `/etc/udev/rules.d/60-canonwebapi.rules`, then reloads udev and triggers Canon devices: - `SUBSYSTEM=="usb", ATTR{idVendor}=="04a9", MODE="0660", GROUP="canonwebapi"`. Desktop users keep their own access - (the `uaccess` ACL is separate from the group). -6. Installs `/etc/systemd/system/canonwebapi.service`, reloads systemd, enables and (re)starts the service, then - prints its status and the URL. +5. Installs `/etc/udev/rules.d/60-canonwebapi.rules`, reloads udev, replays an `add` event for the Canon USB devices + and waits for udev (`udevadm settle`). The rule gives Canon PTP cameras (interface `06/01/01`) `MODE:="0660"`, + `GROUP:="canonwebapi"`: final assignments, because `60-libgphoto2*.rules` sorts after it and sets `MODE="0664", + GROUP="plugdev"` on `add` and `bind` (it skips `change` events, so a `change` trigger would hide the problem until + the next replug or reboot). Canon printers and scanners are untouched. Desktop users keep their own access (the + `uaccess` ACL is separate from the group). +6. Installs `/etc/systemd/system/canonwebapi.service`, reloads systemd, enables and (re)starts the service, waits 3 s + and fails, pointing to `journalctl -u canonwebapi`, when it is not active; then prints the URL and its status. Service unit: @@ -86,16 +93,25 @@ WorkingDirectory=/opt/canonwebapi ExecStart=/opt/canonwebapi/Canon.API Restart=on-failure RestartSec=5 +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=full +ProtectHome=yes [Install] WantedBy=multi-user.target ``` +No `PrivateDevices` (USB camera), `MemoryDenyWriteExecute` (JIT) or `RestrictAddressFamilies` (hot plug uses netlink). +`ProtectSystem=full` leaves `/opt` writable for the logs. A fatal startup error (port in use, invalid +`appsettings.Local.json`) exits with code 1, so `Restart=on-failure` applies. + systemd stops it with SIGTERM: ASP.NET Core shuts down cleanly and closes the camera session. Logs: the API files in `/opt/canonwebapi/logs/` and `journalctl -u canonwebapi` (console output). -Uninstall: stops and disables the service, removes the unit, the udev rule, `/opt/canonwebapi` (settings and logs -included) and the `canonwebapi` user. +Uninstall: stops and disables the service, removes the unit and the udev rule (then replays the `add` event, so a +plugged camera gets its default ownership back), `/opt/canonwebapi` (settings and logs included) and the +`canonwebapi` user. Update: extract the new archive, run its `install.sh`. @@ -114,13 +130,20 @@ PowerShell 7 is installed on the GitHub Windows and Ubuntu runners. commit to build and the version. 2. `windows` (windows-latest) and `linux` (ubuntu-latest), in parallel, both on the prepared commit: restore, build, test, publish their runtime, `Test-Package.ps1`, then archive (`CanonWebAPI.zip` with `Compress-Archive` as today, - `CanonWebAPI-linux-x64.tar.gz` with `tar`) and upload it as a workflow artifact. + `CanonWebAPI-linux-x64.tar.gz` with `tar`, files owned by root in the archive) and upload it as a workflow + artifact. Both time out after 30 minutes. 3. `release` (ubuntu-latest), when both succeeded: downloads the two archives, creates the GitHub release with both - (generated notes, as today), then writes and commits `docs/autoupdate.xml` (Windows zip URL, as today). + (generated notes, as today), then writes and commits `docs/autoupdate.xml` (Windows zip URL and UTF-8 BOM, as + today). + +Token scope: `contents: read` for the workflow; only `prepare` and `release` get `contents: write`. The build jobs run +NuGet build logic and the tests, so their checkout does not persist credentials. -When the Windows or Linux job fails, nothing is released and the feed is not updated. The workflow can also be run by -hand from the Actions tab (`workflow_dispatch`): a dry run of `prepare` (no tag check, no version commit), `windows` and -`linux` that publishes nothing, to check the pipeline on `main` before tagging. +When the Windows or Linux job fails, nothing is released and the feed is not updated. When the `release` job fails +after creating the release, the feed is not updated and the booths stay on the previous version: rerun the failed job. +The workflow can also be run by hand from the Actions tab (`workflow_dispatch`): a dry run of `prepare` (no tag check, +no version commit), `windows`, `linux` and `release` (downloads both packages and generates the XML) that creates no +release and pushes nothing, to check the pipeline on `main` before tagging. `.github/workflows/ci.yml` (pull requests and `main`): the Windows job also publishes win-x64 and runs `Test-Package.ps1`; a new Linux job (ubuntu-latest) restores, builds, tests, publishes linux-x64 and runs From af1484784ce63893ca0f980a60cde064190b0e6f Mon Sep 17 00:00:00 2001 From: Gilbert Paquin Date: Sun, 4 Oct 2026 21:58:50 -0400 Subject: [PATCH 16/16] Bump version to 1.5.0.0 and date the 1.5.0.0 release Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 ++++- Canon.API/Canon.API.csproj | 6 +++--- README.md | 6 ++++-- docs/openapi.json | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72a1a02..3e711df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Versions match the Git tags (`v1.0.0.11`) that trigger the release workflow. ## [Unreleased] +## [1.5.0.0] - 2026-10-04 + ### Added - Linux package in every release: `CanonWebAPI-linux-x64.tar.gz`, a self-contained executable for Linux x64 (no .NET or ICU to install) with the Linux EDSDK, and `install.sh`, which installs it as a systemd service with the USB @@ -262,7 +264,8 @@ First release. - Automatic updates with AutoUpdater.NET, from GitHub releases. - CI/CD workflow building the release package. -[Unreleased]: https://github.com/dansleboby/CanonWebAPI/compare/v1.4.0.0...HEAD +[Unreleased]: https://github.com/dansleboby/CanonWebAPI/compare/v1.5.0.0...HEAD +[1.5.0.0]: https://github.com/dansleboby/CanonWebAPI/compare/v1.4.0.0...v1.5.0.0 [1.4.0.0]: https://github.com/dansleboby/CanonWebAPI/compare/v1.3.0.0...v1.4.0.0 [1.3.0.0]: https://github.com/dansleboby/CanonWebAPI/compare/v1.2.0.0...v1.3.0.0 [1.2.0.0]: https://github.com/dansleboby/CanonWebAPI/compare/v1.1.0.0...v1.2.0.0 diff --git a/Canon.API/Canon.API.csproj b/Canon.API/Canon.API.csproj index 2d826f0..ccd9e47 100644 --- a/Canon.API/Canon.API.csproj +++ b/Canon.API/Canon.API.csproj @@ -8,9 +8,9 @@ x64 - 1.4.0.0 - 1.4.0.0 - 1.4.0.0 + 1.5.0.0 + 1.5.0.0 + 1.5.0.0 diff --git a/README.md b/README.md index ea2f265..a6ecca7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A web API for remotely controlling Canon DSLR and mirrorless cameras. This project utilizes the Canon EDSDK to communicate with the camera and includes automatic update capabilities. -**Current version: 1.4.0.0** (.NET 10, Canon EDSDK 13.20.21). See the [changelog](CHANGELOG.md) for the changes of each version. +**Current version: 1.5.0.0** (.NET 10, Canon EDSDK 13.20.21). See the [changelog](CHANGELOG.md) for the changes of each version. > **Upgrading from 1.0.x**: errors are now returned as problem details with new status codes (e.g. 503 when the camera is not connected, 504 instead of 408 on capture timeout), and values unknown to the value tables are returned in hexadecimal (`"0x99"`). See the [changelog](CHANGELOG.md#1100---2026-09-30). @@ -12,6 +12,8 @@ A web API for remotely controlling Canon DSLR and mirrorless cameras. This proje > **Upgrading from 1.3.x**: settings changed in `appsettings.json` are reset by this update: put them in `appsettings.Local.json`, which updates never overwrite (see [Configuration](#configuration)). `POST /settings` refuses unknown fields (400), and f/4.5 in 1/3 stop steps now reads `"4.5 (1/3)"` (sending `"4.5"` still works). See the [changelog](CHANGELOG.md#1400---2026-09-30). +> **Upgrading from 1.4.x**: the "flash firing" setting is no longer written while the live view runs (it froze the EOS R100): with `Canon:ForceFlashFiring` it is set when the camera connects, before the live view starts and before a capture taken without live view, and a value set with `POST /flash` stays until the live view stops. Linux is supported (see [Installing on Linux](#installing-on-linux)). From the sources, `dotnet run` and `dotnet publish` need `-f net10.0-windows` or `-f net10.0`. See the [changelog](CHANGELOG.md#1500---2026-10-04). + ## Features * Get camera information (e.g., camera name, shooting mode, temperature restrictions). @@ -241,7 +243,7 @@ dotnet run --project Canon.API -f net10.0-windows # on Linux: -f net10.0 This project uses automated GitHub Actions for releases: 1. **Changelog**: Move the changes of the `Unreleased` section of [CHANGELOG.md](CHANGELOG.md) under the new version and date, and update the version in `Canon.API/Canon.API.csproj` -2. **Tag-based releases**: Push a tag like `v1.4.0.0` on the latest commit of `main` to trigger automated build and release (the workflow refuses a tag on another commit) +2. **Tag-based releases**: Push a tag like `v1.5.0.0` on the latest commit of `main` to trigger automated build and release (the workflow refuses a tag on another commit) 3. **Version synchronization**: The workflow automatically updates project versions to match the tag 4. **Packages**: The workflow builds and tests the solution, publishes the Windows and Linux packages and starts both to check that they load the EDSDK (`tools/Test-Package.ps1`) 5. **Atomic release**: Only when both packages succeeded, it creates the release (`CanonWebAPI.zip`, `CanonWebAPI-linux-x64.tar.gz`), then updates the AutoUpdater XML. A failure of the build jobs publishes nothing; if the release job fails after creating the release, the feed is not updated and the photo booths stay on the previous version: rerun the failed job diff --git a/docs/openapi.json b/docs/openapi.json index a25addb..455e9f2 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Canon Web API", "description": "Remote control of a Canon camera through the Canon EDSDK: settings, capture and MJPEG live view. Errors are returned as problem details: 400 invalid value, 409 capture refused by the camera or setting not changeable in the current shooting mode, 503 camera not connected or busy, 504 capture timeout.", - "version": "1.4.0" + "version": "1.5.0" }, "paths": { "/cameraname": {