Skip to content

feat: Windows display name, clean-reinstall, macOS ensure-hosts, 3-OS service smoke CI#34

Merged
MichaelTaylor3d merged 1 commit into
mainfrom
feat/service-install-hardening
Jul 13, 2026
Merged

feat: Windows display name, clean-reinstall, macOS ensure-hosts, 3-OS service smoke CI#34
MichaelTaylor3d merged 1 commit into
mainfrom
feat/service-install-hardening

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Hardens the OS-service install path across Windows/macOS/Linux (#494, #502).

  • Clean-reinstallinstall is now stop -> delete -> wait -> create (never a bare create),
    so re-running it against an already-registered service no longer hits Windows CreateService
    error 1073. Deliberately never auto-starts the service (unlike the dig-dns pattern this
    mirrors): the dig-installer issues its own start afterward and treats a start failure as
    fatal for that step, so an auto-started install could flip its reported outcome to failed.
  • Windows display namesc config sets the SCM display name to "DIG NETWORK: NODE" after
    create, then sc qc reads it back to confirm the override actually took
    (result.display_name_verified in --json). Verified LIVE in CI (see below).
  • Real bug caught + fixed by the new CI job — the existing-service probe originally used
    to_qualified_name() uniformly, but service-manager's systemd backend actually registers
    under to_script_name() instead (a silent divergence with no compile-time signal). This made
    is_installed() always report false on Linux, defeating clean-reinstall there. Fixed +
    regression-tested.
  • macOS ensure-hostspostinstall now calls the binary's own idempotent ensure-hosts so
    dig.local -> 127.0.0.2 is registered on .pkg install (Windows MSI + the .deb already did
    this).
  • New service-smoke CI job (windows-latest/macos-14/ubuntu-latest): build -> install ->
    start -> poll for serving -> assert the Windows display name -> install a SECOND time (proves
    no 1073) -> re-assert the display name -> start -> poll -> stop -> uninstall -> assert the
    registration is gone.
  • config::DEFAULT_PORT now sources dig_constants::DIG_NODE_PORT (single-sourced §5.3 default)
    via a rev-pinned dig-constants 0.3.0 dependency — kept a cargo-DISTINCT source from
    dig-node-core's bare-git 0.2.x pin so the P2P crate chain (dig-nat/dig-gossip/dig-dht/dig-onion)
    does not need to move in this PR.
  • SPEC.md documents the display-name + clean-reinstall contract and the previously-undocumented
    9444 DIG_PEER_PORT P2P listener default; DEVELOPMENT_LOG.md records the durable gotchas
    found along the way (bare-git version unification, the installer's install->start sequencing
    constraint, the systemd script-name divergence).

Minor version bump: 0.28.0 -> 0.29.0 (new capability, no breaking change).

Blast radius checked (gitnexus-style manual analysis — MCP tool unavailable this session)

  • service::SERVICE_LABEL — only consumer is win_service.rs (SCM dispatcher registration);
    value unchanged.
  • service::install/uninstall/start/stop/status — only consumer is main.rs's CLI dispatch;
    signatures unchanged (io::Result<Outcome>), so main.rs needed no changes.
  • config::DEFAULT_PORT — confined to config.rs (default + from_env + tests); no other file
    references it.
  • Cross-repo: dig-installer's register_dig_node/install_service sequencing was the reason
    install intentionally does NOT auto-start (see the module doc + DEVELOPMENT_LOG.md) — no code
    change needed there since the existing "install, then separately start" sequence still works.

Test plan

  • cargo test -p dig-node-service --lib — 154 passed (was 153; +1 regression test), incl. new
    service::tests for the clean-reinstall mock, the sc-qc display-name read-back parser, and the
    systemd script-name-vs-qualified-name regression.
  • cargo test -p dig-node-service --test server — 45 passed (integration suite unaffected).
  • cargo fmt --all -- --check / cargo clippy -p dig-node-service --all-targets --locked -- -D warnings — clean (also exercises the #[cfg(windows)] branches directly, since this session runs on Windows).
  • Real 3-OS CI evidence (this PR, squashed commit db95aed):
    • Run https://github.com/DIG-Network/dig-node/actions/runs/29246301933install/start/reinstall/uninstall on windows-latest / macos-14 / ubuntu-latest, all pass.
    • Windows leg: sc qc shows DISPLAY_NAME : DIG NETWORK: NODE; install-2.json carries "reinstalled":true,"display_name_verified":true; uninstall.json carries "installed":false.
    • Linux/macOS legs: install-2.json carries "reinstalled":true (the 1073-equivalent clean-reinstall path exercised for real).
    • All other required gates green: CI (Rustfmt/Clippy/Test+coverage), Native install packages (.msi/.pkg/.deb), Check version increment, Commitlint, CodeQL.

Closes #494 (node-half), refs #502.

Co-Authored-By: Claude noreply@anthropic.com

Comment thread .github/workflows/service-smoke.yml Fixed
Comment on lines +35 to +163
name: install/start/reinstall/uninstall (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-14, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2
with:
key: service-smoke

- name: Build dig-node (release)
run: cargo build --release --locked --bin dig-node

# `dig-node install` registers a USER-level systemd unit on Linux (no root needed), which
# needs a real user D-Bus session — the GH-hosted runner's non-interactive job shell does
# not start one implicitly. `enable-linger` + pointing at the per-uid runtime dir brings one
# up so `systemctl --user ...` (what service-manager shells out to) has a bus to talk to.
- name: (Linux) bring up a user systemd/D-Bus session
if: runner.os == 'Linux'
run: |
sudo loginctl enable-linger "$(whoami)"
echo "XDG_RUNTIME_DIR=/run/user/$(id -u)" >> "$GITHUB_ENV"

- name: Resolve the built binary path
id: bin
shell: bash
run: |
if [ "${{ runner.os }}" = "Windows" ]; then
echo "path=target/release/dig-node.exe" >> "$GITHUB_OUTPUT"
else
echo "path=target/release/dig-node" >> "$GITHUB_OUTPUT"
fi

- name: Install (1st) — fresh registration
shell: bash
run: |
"${{ steps.bin.outputs.path }}" install --json | tee install-1.json
grep -q '"installed":true' install-1.json
grep -q '"reinstalled":false' install-1.json

- name: Windows — assert the SCM display name
if: runner.os == 'Windows'
shell: bash
run: |
sc.exe qc net.dignetwork.dig-node | tee sc-qc-1.txt
grep -Eq 'DISPLAY_NAME[[:space:]]*: DIG NETWORK: NODE' sc-qc-1.txt

- name: Start + poll for RUNNING
shell: bash
run: |
"${{ steps.bin.outputs.path }}" start --json || true
ok=0
for _ in $(seq 1 20); do
if "${{ steps.bin.outputs.path }}" status --json | tee status-1.json | grep -q '"serving":true'; then
ok=1
break
fi
sleep 1
done
test "$ok" -eq 1

- name: Install (2nd) — proves clean-reinstall avoids CreateService 1073
shell: bash
run: |
"${{ steps.bin.outputs.path }}" install --json | tee install-2.json
grep -q '"installed":true' install-2.json
grep -q '"reinstalled":true' install-2.json

- name: Windows — re-assert the SCM display name after reinstall
if: runner.os == 'Windows'
shell: bash
run: |
sc.exe qc net.dignetwork.dig-node | tee sc-qc-2.txt
grep -Eq 'DISPLAY_NAME[[:space:]]*: DIG NETWORK: NODE' sc-qc-2.txt

- name: Start again + poll for RUNNING
shell: bash
run: |
"${{ steps.bin.outputs.path }}" start --json || true
ok=0
for _ in $(seq 1 20); do
if "${{ steps.bin.outputs.path }}" status --json | tee status-2.json | grep -q '"serving":true'; then
ok=1
break
fi
sleep 1
done
test "$ok" -eq 1

- name: Stop
shell: bash
run: |
"${{ steps.bin.outputs.path }}" stop --json

- name: Uninstall + assert the service is gone
shell: bash
run: |
"${{ steps.bin.outputs.path }}" uninstall --json | tee uninstall.json
grep -q '"installed":false' uninstall.json
if [ "${{ runner.os }}" = "Windows" ]; then
! sc.exe query net.dignetwork.dig-node
elif [ "${{ runner.os }}" = "macOS" ]; then
! launchctl print "gui/$(id -u)/net.dignetwork.dig-node"
else
! systemctl --user cat net.dignetwork.dig-node.service
fi

- name: Upload diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: service-smoke-${{ matrix.os }}
path: |
install-1.json
install-2.json
status-1.json
status-2.json
uninstall.json
sc-qc-1.txt
sc-qc-2.txt
if-no-files-found: ignore
…sts, 3-OS smoke CI

- install now clean-reinstalls (stop -> delete -> wait -> create) instead of a bare
  create, so re-running it against an already-registered service no longer hits
  Windows CreateService error 1073. It deliberately never auto-starts (unlike the
  dig-dns pattern this mirrors), since the dig-installer issues its own start
  afterward and treats a start failure as fatal for that step.
- Windows: sc config sets the SCM display name to "DIG NETWORK: NODE" after create,
  then sc qc reads it back to confirm the override actually took
  (result.display_name_verified in --json). Verified live in CI: sc qc reports
  DISPLAY_NAME: DIG NETWORK: NODE and display_name_verified:true.
- The existing-service probe uses the identifier service-manager's OWN backend
  actually registers under: to_qualified_name() for Windows/launchd, but
  to_script_name() for systemd, whose unit-file naming diverges silently. Caught by
  a real ubuntu-latest smoke run (a second install reported reinstalled:false until
  fixed); regression test added.
- macOS postinstall now calls the binary's own idempotent `ensure-hosts` so
  dig.local -> 127.0.0.2 is registered on .pkg install (Windows/.deb already did
  this).
- New service-smoke CI job (windows-latest/macos-14/ubuntu-latest): build, install,
  start, poll for serving, assert the Windows display name, install a second time to
  prove no 1073, stop, uninstall, and assert the registration is gone. All three
  legs green against the fix.
- config::DEFAULT_PORT now sources dig_constants::DIG_NODE_PORT (single-sourced
  §5.3 default) via a rev-pinned dig-constants 0.3.0 dependency, kept a cargo-
  distinct source from dig-node-core's bare-git 0.2.x pin so the P2P crate chain
  (dig-nat/dig-gossip/dig-dht/dig-onion) does not need to move in this PR.
- SPEC.md: document the display-name + clean-reinstall contract (#494), and the
  previously-undocumented 9444 DIG_PEER_PORT P2P listener default.
- DEVELOPMENT_LOG.md: durable realizations on the bare-git version-unification
  gotcha, the installer's install->start sequencing constraint, and the systemd
  script-name vs qualified-name divergence.

Minor version bump (0.28.0 -> 0.29.0): new capability, no breaking change.

Closes #494, refs #502.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d MichaelTaylor3d force-pushed the feat/service-install-hardening branch from 21ae332 to db95aed Compare July 13, 2026 11:27
@MichaelTaylor3d MichaelTaylor3d marked this pull request as ready for review July 13, 2026 11:37
@MichaelTaylor3d MichaelTaylor3d merged commit 240c576 into main Jul 13, 2026
17 checks passed
@MichaelTaylor3d MichaelTaylor3d deleted the feat/service-install-hardening branch July 13, 2026 11:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants