Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 188 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Linux and macOS builds. PyInstaller does not cross-compile, so each is
# made on its own runner by tools/build_unix.py, then STARTED: the build is
# kept only if the program is still running 20 seconds later, and a screen
# shot of it is kept with the build (artifacts). Windows builds are made by
# hand with tools/build_installer.py.
# The release builds. PyInstaller does not cross-compile, so each system is
# built on its own runner, then STARTED: the build is kept only if the
# program is still running 20 seconds later, and a screen shot of it is kept
# with the build (artifacts).
#
# windows tools/build_installer.py — the installer and the two portable
# exes, signed by SignPath Foundation (see the job)
# linux tools/build_unix.py — the AppImage
# macos tools/build_unix.py — ModbusReader.app in a zip
#
# Runs for a version tag and by hand (Actions → build → Run workflow).
name: build
Expand All @@ -16,6 +20,185 @@ permissions:
contents: read

jobs:
windows:
# Code signing (https://modbusreader.ru/code-signing/): SignPath signs
# only what this job built from the tagged source, and only after a
# person approved the request on app.signpath.io — twice per build:
# 1. the programs: the one-folder app, the launcher, the portable exes
# (the installer must carry them signed, so it is built after);
# 2. the installer.
# On while the repository has the variable SIGNPATH_ORGANIZATION_ID
# (and the secret SIGNPATH_API_TOKEN); without it the same files are
# built unsigned. installer/signpath/ holds the two artifact
# configurations and the set-up steps.
runs-on: windows-latest
timeout-minutes: 150 # mostly waiting for the two approvals
permissions:
contents: read
actions: read # SignPath fetches the unsigned artifacts
env:
PYTHONUTF8: "1"
SIGNPATH_POLICY: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG || 'release-signing' }}
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip

- name: Dependencies (the pinned versions)
run: python -m pip install -r requirements-dev.txt

# The style of installer/ModbusReader.iss needs a current Inno Setup,
# whatever the runner image carries.
- name: Inno Setup
run: choco upgrade innosetup --no-progress -y

- name: Tests
run: python -m pytest -q -p no:cacheprovider

- name: Build the programs
run: python tools/build_installer.py --portable --only programs

# Four files, three of them named ModbusReader.exe: one folder each.
# installer/signpath/programs.xml lists the same paths.
- name: Collect the programs for signing
if: vars.SIGNPATH_ORGANIZATION_ID != ''
shell: pwsh
run: |
$s = "signing/programs"
New-Item -ItemType Directory -Force "$s/program", "$s/launcher", "$s/portable" | Out-Null
Copy-Item dist/ModbusReader/ModbusReader.exe "$s/program/"
Copy-Item build/launcher/ModbusReader.exe "$s/launcher/"
Copy-Item dist/ModbusReader.exe, dist/ModbusReader_debug.exe "$s/portable/"

- id: unsigned-programs
if: vars.SIGNPATH_ORGANIZATION_ID != ''
uses: actions/upload-artifact@v7
with:
name: unsigned-programs
path: signing/programs
retention-days: 1
overwrite: true

- name: Sign the programs (approve on app.signpath.io)
if: vars.SIGNPATH_ORGANIZATION_ID != ''
uses: signpath/github-action-submit-signing-request@v3
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG || 'ModbusReader' }}
signing-policy-slug: ${{ env.SIGNPATH_POLICY }}
artifact-configuration-slug: programs
github-artifact-id: ${{ steps.unsigned-programs.outputs.artifact-id }}
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: signing/programs-signed

- name: Put the signed programs in place
if: vars.SIGNPATH_ORGANIZATION_ID != ''
shell: pwsh
run: |
$s = "signing/programs-signed"
Copy-Item -Force "$s/program/ModbusReader.exe" dist/ModbusReader/
Copy-Item -Force "$s/launcher/ModbusReader.exe" build/launcher/
Copy-Item -Force "$s/portable/ModbusReader.exe", "$s/portable/ModbusReader_debug.exe" dist/

- name: Build the installer
run: python tools/build_installer.py --only installer

# The whole way a user goes: install for the current user (no rights
# needed), start the launcher, which starts the version folder.
- name: Install it and start it
shell: pwsh
run: |
$setup = Start-Process dist/ModbusReader-Setup.exe -PassThru -Wait -ArgumentList `
'/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/CURRENTUSER', '/NOLAUNCH'
if ($setup.ExitCode) { Write-Output "::error::the installer exited with $($setup.ExitCode)"; exit 1 }
$root = "$env:LOCALAPPDATA\Programs\ModbusReader"
Start-Process "$root\ModbusReader.exe"
Start-Sleep 20
try {
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
$bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height
[System.Drawing.Graphics]::FromImage($bmp).CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size)
$bmp.Save("$PWD\dist\screen-windows.png")
} catch { Write-Output "no screen shot: $_" }
$app = Get-Process ModbusReader -ErrorAction SilentlyContinue |
Where-Object { $_.Path -like "$root\app-*" }
if (-not $app) { Write-Output "::error::the program exited"; exit 1 }
$app | Stop-Process -Force
$log = "$env:APPDATA\ModbusReader\modbusreader_errors.log"
if ((Test-Path $log) -and (Get-Item $log).Length) { Get-Content $log; exit 1 }

- id: unsigned-installer
if: vars.SIGNPATH_ORGANIZATION_ID != ''
uses: actions/upload-artifact@v7
with:
name: unsigned-installer
path: dist/ModbusReader-Setup.exe
retention-days: 1
overwrite: true

- name: Sign the installer (approve on app.signpath.io)
if: vars.SIGNPATH_ORGANIZATION_ID != ''
uses: signpath/github-action-submit-signing-request@v3
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG || 'ModbusReader' }}
signing-policy-slug: ${{ env.SIGNPATH_POLICY }}
artifact-configuration-slug: installer
github-artifact-id: ${{ steps.unsigned-installer.outputs.artifact-id }}
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: signing/installer-signed

- name: Put the signed installer in place
if: vars.SIGNPATH_ORGANIZATION_ID != ''
run: Copy-Item -Force signing/installer-signed/ModbusReader-Setup.exe dist/

# Each file with its signature and SHA-256 (what GitHub will show for
# the release asset). When signing is on, a file without a valid
# signature fails the build; SignPath's test-signing policy signs with
# a certificate Windows does not trust, so then it only has to be there.
- name: Signatures
shell: pwsh
env:
SIGNING: ${{ vars.SIGNPATH_ORGANIZATION_ID != '' }}
run: |
$bad = 0
foreach ($f in 'dist/ModbusReader-Setup.exe', 'dist/ModbusReader.exe', 'dist/ModbusReader_debug.exe',
'dist/ModbusReader/ModbusReader.exe', 'build/launcher/ModbusReader.exe') {
$sig = Get-AuthenticodeSignature $f
$hash = (Get-FileHash $f -Algorithm SHA256).Hash.ToLower()
'{0,-36} {1,-13} {2} {3}' -f $f, $sig.Status, $hash, $sig.SignerCertificate.Subject
$ok = if ($env:SIGNPATH_POLICY -eq 'test-signing') { $sig.SignerCertificate -and $sig.Status -ne 'HashMismatch' }
else { $sig.Status -eq 'Valid' }
if ($env:SIGNING -eq 'true' -and -not $ok) { $bad++ }
}
if ($bad) { Write-Output "::error::$bad file(s) without a valid signature"; exit 1 }

- uses: actions/upload-artifact@v7
with:
name: windows
# All in dist/, so the artifact holds them without a folder.
path: |
dist/ModbusReader-Setup.exe
dist/ModbusReader.exe
dist/ModbusReader_debug.exe
dist/screen-windows.png
if-no-files-found: error
retention-days: 30

- uses: actions/upload-artifact@v7
if: failure()
with:
name: windows-failed
path: dist/screen-windows.png
if-no-files-found: ignore
retention-days: 30

linux:
# The oldest supported Ubuntu: an AppImage runs on systems with the same
# or a NEWER glibc than the one it was built on, never an older one.
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# The test suite on Linux and macOS (Windows is where the releases are built
# and tested by hand). Each run also photographs every screen headless
# The test suite on Linux and macOS (on Windows it runs in the release build,
# .github/workflows/build.yml). Each run also photographs every screen headless
# (tools/ui_snapshots.py) and keeps the pictures as an artifact, to see how
# the window looks with that system's fonts.
#
Expand Down
23 changes: 18 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,9 +429,20 @@ pictures, from the demo project).
| `installer/ModbusReader.iss` | the installer (Inno Setup 6) |
| `ModbusReader_unix.spec` | Linux and macOS: the program as a folder / `ModbusReader.app` |

Windows builds are made locally by `tools/build_installer.py`. Linux and
macOS builds cannot be (PyInstaller does not cross-compile):
`.github/workflows/build.yml` runs `tools/build_unix.py` on an Ubuntu 22.04
Release builds come from `.github/workflows/build.yml`, one runner per
system (PyInstaller does not cross-compile). **Windows**: the tests, then
`tools/build_installer.py` in two halves — `--portable --only programs`
(the one-folder program, the launcher, the portable exes) and
`--only installer` (ISCC) — with a SignPath signing request after each, so
the installer carries signed files and is signed itself; then a silent
per-user install, a start of the launcher and the same 20-second check as
below. SignPath Foundation signs only artifacts of this workflow, after a
person approves each request; the job signs while the repository has the
variable `SIGNPATH_ORGANIZATION_ID` and builds the same files unsigned
without it (`installer/signpath/` — the artifact configurations and the
set-up; the public policy is `docs/code-signing.md`). A local
`tools/build_installer.py` build is for testing: it is never signed.
**Linux and macOS**: `tools/build_unix.py` on an Ubuntu 22.04
runner — `ModbusReader-x86_64.AppImage`, built on the oldest supported glibc
so it runs on newer ones — and a macOS arm64 runner —
`ModbusReader-macOS-arm64.zip`, ad-hoc signed, not notarized. Each build is
Expand Down Expand Up @@ -471,7 +482,9 @@ commit passes the test suite.
## 11. Known limitations

- Polling reads one register per request; block reads are not implemented yet.
- Builds are not code-signed, so Windows SmartScreen warns on first
installation.
- Windows builds are signed only once SignPath Foundation has accepted the
project; until then, and for a new version with few downloads, Windows
SmartScreen warns on first installation. Linux and macOS builds are not
signed (macOS: ad-hoc only).
- `ModernScadaApp` is still the largest module and the natural next candidate
for splitting (project handling, overlay management).
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
неверной CRC, оборванный или искажённый кадр учитывается как ошибка CRC, а
не как таймаут; эхо адаптера RS-485 и помехи перед ответом обрабатываются;
ответ читается сразу, как только он полностью пришёл.
- Файлы для Windows собирает GitHub Actions из исходного кода выпуска, а не
компьютер разработчика: так их сможет подписывать SignPath Foundation
(бесплатная подпись для открытых проектов). Что подписывается и какие
данные программа передаёт по сети — в
[политике подписи кода](https://modbusreader.ru/code-signing/).

### Исправлено
- Демо-проект в английском интерфейсе показывал русские единицы, подписи
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,11 @@ Linux / macOS: те же шаги (`source .venv/bin/activate`, `python main.py`
`ModbusReader.exe` / `ModbusReader_debug.exe` (с консолью). Сведения о версии
exe берутся из `app/version.py`; иконка и заставка — `tools/make_icon.py`,
`tools/make_splash.py`.

Локальная сборка — для проверки. Файлы выпуска собирает GitHub Actions
(`.github/workflows/build.yml`): тесты, сборка в два этапа (`--only programs`,
затем `--only installer`), подпись через SignPath после каждого этапа, пробная
установка и запуск. Настройка подписи — [installer/signpath/](installer/signpath/README.md).
</details>

<details>
Expand All @@ -310,7 +315,8 @@ CHANGELOG в раздел выпуска со ссылкой на него.

В репозитории один выпуск — всегда последний: новая версия заменяет в нём
файлы `ModbusReader-Setup.exe`, `ModbusReader.exe` и `ModbusReader_debug.exe`
и переносит его на свой тег `vX.Y.Z` (не pre-release). Теги всех версий
(подписанные, из сборки GitHub Actions для тега) и переносит его на свой тег
`vX.Y.Z` (не pre-release). Теги всех версий
остаются в истории, а выпуск — один. Скачанное программа сверяет с SHA-256,
который GitHub считает при загрузке файла.

Expand Down Expand Up @@ -377,6 +383,14 @@ ModbusReader бесплатен и таким останется. Если пр
вопросы по работе с программой — в [Discussions](https://github.com/Mercurrial/ModbusReader/discussions).
О найденных уязвимостях сообщайте по правилам [SECURITY.md](SECURITY.md).

## Подпись кода

Бесплатная подпись кода предоставлена [SignPath.io](https://about.signpath.io/),
сертификат — [SignPath Foundation](https://signpath.org/). Файлы для Windows
собирает GitHub Actions из исходного кода выпуска; что подписывается, кто
утверждает выпуски и какие данные программа передаёт по сети — в
[политике подписи кода](https://modbusreader.ru/code-signing/).

## Лицензия

ModbusReader — свободная программа: её можно использовать (в том числе в
Expand Down
9 changes: 7 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
GitHub и устанавливает файл, лишь если его SHA-256 совпадает с контрольной
суммой, которую GitHub публикует для файла выпуска. Любой обход этой
проверки — уязвимость.
- **Подпись выпусков.** Файлы для Windows подписываются сертификатом
SignPath Foundation только после сборки в GitHub Actions и ручного
утверждения ([политика подписи кода](https://modbusreader.ru/code-signing/)).
Способ получить такую подпись в обход этой сборки — уязвимость.
- **Открытие файлов.** Карты регистров (`.json`), проекты (`.mbproj`),
дашборды (`.mbdash`), снимки (`.mbsnap`) и импортируемые таблицы могут
прийти от третьих лиц. Выполнение кода или запись за пределами выбранного
Expand All @@ -50,5 +54,6 @@
- Протокол Modbus RTU не предусматривает аутентификации и шифрования; любой,
кто подключён к линии RS-485, может читать и записывать регистры. Это
свойство протокола, а не программы.
- Предупреждение Windows SmartScreen при первом запуске: сборки пока не
подписаны сертификатом разработчика.
- Предупреждение Windows SmartScreen при первом запуске: неподписанные
выпуски (до подключения подписи) и только что вышедшая версия, у которой
ещё мало скачиваний.
3 changes: 3 additions & 0 deletions core/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,9 @@ def start_detached(args: list[str]) -> subprocess.Popen:
| getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0))
kw = dict(stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, close_fds=True, env=child_env())
if sys.platform != "win32":
# creationflags is Windows-only (Popen raises ValueError elsewhere).
return subprocess.Popen(args, start_new_session=True, **kw)
try:
return subprocess.Popen(args, creationflags=flags | 0x01000000, **kw)
except OSError:
Expand Down
Loading
Loading