Skip to content

feat-cleanup: protocol split, build-tag gating, lifecycle hardening, CI matrix#6

Merged
pgodwin merged 116 commits intomainfrom
feat-cleanup
May 1, 2026
Merged

feat-cleanup: protocol split, build-tag gating, lifecycle hardening, CI matrix#6
pgodwin merged 116 commits intomainfrom
feat-cleanup

Conversation

@pgodwin
Copy link
Copy Markdown
Contributor

@pgodwin pgodwin commented May 1, 2026

Summary

Large cleanup branch (113 commits) consolidating several long-running threads of work.

Wire-format / protocol split

  • New protocol/{atp,asp,zip,rtmp,aep,nbp,llap} packages own pure wire format; service packages now consume them as canonical types (renamed ATPHeaderHeader, DDPTypeATPDDPType, etc.).
  • AFP response models migrated to MarshalWire/WireSize with golden hex tests; volume/info/fork packing routed through binutil.
  • pkg/encoding, pkg/appledouble, pkg/telemetry lifted out of service code.

Build-tag gating + variants

  • //go:build gates for afp, macgarden, macip, sqlite_cnid, plus a unifying all tag.
  • New router-only release variant — CI now publishes both omnitalk-* (full) and omnitalk-router-* archives per OS.
  • PR CI strengthened: vet, race, golangci-lint, govulncheck, gosec, and a build-tag matrix.

Lifecycle / concurrency hardening

  • Service contexts derived from Start parent; long-running waits honour shutdown signals.
  • Background goroutines tracked via WaitGroup across macip, dsi, asp, llap, rtmp, zip.
  • errors.Join for aggregated Stop errors; LLAP CTS wait honours ctx cancellation.

AFP refactor

  • Service god-struct decomposed into sessionState, desktopState, forkState, backupDates substructs; Service.mu removed.
  • 525-LOC HandleCommand switch replaced with a command registry; fs.go split by concern.
  • AppleDouble format moved to pkg/appledouble; macgarden VFS moved out of core AFP.

Config + logging

  • INI replaced with TOML via koanf; AFP/EtherTalk/LocalTalk own their own Config schemas.
  • netlog formalised as the project logging API.

Tests + docs

  • Fuzz tests for ATP/NBP/LLAP/DDP/ASP decoders; ATP wire benchmarks.
  • ARCHITECTURE.md added; package doc comments throughout.

Test plan

  • PR CI green (test, quality, build-tag matrix, build on all three OSes)
  • Smoke-test full binary against a real Mac with AFP + MacIP
  • Smoke-test router-only binary forwarding RTMP/ZIP between EtherTalk and LToUDP
  • Verify both release archives produced on a tag push (dev tag first)

pgodwin and others added 30 commits April 23, 2026 17:13
Rename module from `github.com/pgodw/omnitalk/go` to
`github.com/pgodw/omnitalk`. The repository root is the module root;
the `/go` suffix was redundant and confusing for tooling and
newcomers.

All import sites rewritten across the codebase. Go doc comments in
CLAUDE.md updated to match.

Step 0 of the architectural refactor plan
(.claude/plans/review-the-architectural-layout-splendid-squirrel.md).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The appletalk/ package was a thin re-export of protocol/ddp/ and
encoding/ types, adding no value beyond a layer of aliases. Every
consumer now imports the underlying package directly.

- appletalk.Datagram → ddp.Datagram
- appletalk.DDPChecksum → ddp.Checksum
- appletalk.MaxDataLength → ddp.MaxDataLength
- appletalk.DatagramFromLongHeaderBytes  → ddp.DatagramFromLongHeaderBytes
- appletalk.DatagramFromShortHeaderBytes → ddp.DatagramFromShortHeaderBytes
- appletalk.MacRomanToUpper → encoding.MacRomanToUpper
- appletalk.MacRomanToUTF8  → encoding.MacRomanToUTF8
- appletalk.UTF8ToMacRoman  → encoding.UTF8ToMacRoman
- appletalk.Packet → new protocol.Packet (in new protocol/ package)

The Packet interface moves to the new top-level protocol/ package,
which will host cross-protocol contracts as the wire-format code is
lifted out of service/ in later steps.

Step 1 of the architectural refactor plan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces pkg/hwaddr/ with Ethernet, LocalTalk, and AppleTalk types
plus parsing, formatting, generation, and conversion helpers.
Consolidates duplicated MAC parsing previously in cmd/omnitalk's
parseMAC and service/macip's fabricateMACForAT.

- cmd/omnitalk/main.go: parseMAC replaced with hwaddr.ParseEthernet;
  helper deleted.
- service/macip/dhcp_client.go: fabricateMACForAT delegates to
  hwaddr.MacIPEthernetFromAppleTalk, preserving the exact on-wire
  layout used by existing DHCP leases (0x02 | net | node | 'M' | 'I').
- Round-trip and validation tests in pkg/hwaddr/.

Further call-site migrations (ethertalk.New accepting hwaddr.Ethernet,
port/localtalk GenerateLocalTalk adoption) will land alongside Step 10
(BridgeConfigurable / ethertalk.Options) where the surrounding
signatures change anyway.

Step 2 of the architectural refactor plan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Creates internal/testutil/ with exported MockPort and MockRouter plus
NewMockPort/NewMockRouter constructors. Replaces the two duplicated
mock files that previously lived in service/ and service/zip/.

- service/mock_router_port_test.go: deleted. The service package had
  no test consumers; this file was dead code.
- service/zip/mock_test.go: reduced to a pair of type aliases so the
  existing zip tests keep compiling under their familiar names.
- service/zip/name_information_test.go: mock factories now delegate
  to testutil.NewMockPort/NewMockRouter; field-mutation sites updated
  to exported casing.

NewAFPHarness is intentionally deferred to Step 15 when AFP
decomposition lands; building it now would freeze an API that step
will reshape.

Step 4 of the architectural refactor plan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Eight stdlib log.Printf calls replaced with netlog.Info / netlog.Warn
so every port/ethertalk log line flows through the project's shared
logger. The "log" stdlib import is removed from the package.

netlog remains the interim transport; Step 6 replaces it with
pkg/logging (slog-based) and Step 7 migrates all call sites.

Step 5 of the architectural refactor plan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pkg/logging is a thin slog wrapper providing per-component source tags
([AFP], [ASP], ...) rendered as a prefix in console output and emitted as
"source":"AFP" in JSON, with dual-mode sink fan-out so console and JSON
can run simultaneously. pkg/logging/protolog is an opt-in channel for raw
wire traffic (per-source, per-direction filtering; console, JSON sinks).

netlog stays functional as a shim writing through stdlib log so every
existing call site keeps its current behaviour until step 7 migrates
them. The netlog package body is otherwise untouched; only the header
comment changes to mark it deprecated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rather than thread a *slog.Logger through every service constructor (a
30-file, 349-call-site mechanical rewrite that would stall the rest of
the refactor), wire the netlog shim to forward to a slog.Logger that
main.go installs at startup. A pkg/logging root logger is built with a
console sink at the configured level, set as slog.Default, and handed
to netlog.SetLogger.

The shim keeps dual-mode behaviour: when no logger is installed (e.g.
in tests that haven't initialised slog), Debug/Info/Warn still emit
through stdlib log for backward-compat with tests that capture
log.SetOutput. Once SetLogger runs, every netlog call flows through
slog with the "source":"OmniTalk" attribute; sub-components get their
own source tags in a follow-up pass that replaces netlog call sites
with logger.With("source", "AFP")-style constructors.

This delivers the plan's behavioural outcome (structured slog output
with handler fan-out available) while deferring the ergonomic
constructor threading to a later commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
INI parsing and defaulting move out of cmd/omnitalk into a new
top-level config/ package. config.Root, config.LoadINI, and
config.Defaults are the entry points; cmd/omnitalk keeps a thin
compat shim aliasing config.Root as iniConfig so main.go and its
tests continue to compile without a mass rename pass.

This is the lift-and-shift cut. Later commits split Root into per-
service subtrees (RouterConfig, AFPConfig, etc.) and add Validate
methods; the move here is what unblocks those later cuts, since
services may now import config without depending on cmd/omnitalk.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The AFP-agnostic pieces of service/afp/cnid.go and sqlite_store.go move
to pkg/cnid: the Store interface, MemoryStore, SQLiteStore, and the
SQLite helper (OpenSQLiteDB, SQLitePath, SQLiteFilename). Constants
are renamed to drop the CNID prefix (cnid.Root, cnid.Invalid,
cnid.ParentOfRoot) since the package name already provides that
context.

service/afp keeps the AFP-coupled backend adapters (MemoryCNIDBackend,
SQLiteCNIDBackend) and aliases the pkg/cnid types so the ~16 existing
callers inside AFP (volume, fork, directory, path_codec, tests) keep
compiling unchanged. The SQLite Desktop DB helper now calls
cnid.OpenSQLiteDB directly so the same per-volume database can back
both CNID and Desktop storage without duplicating the pragma setup.

Follow-ups: tighten the backend registry (plan step 21) so third-
party filesystems can plug their own Store implementation; move the
CNID backend selection out of AFPOptions into the volume config
subtree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… PcapPort in main

Step 10 of the architectural refactor. Introduces port.BridgeConfigurable as
an optional interface for ports that need operator control over Ethernet
bridge mode and host-MAC synthesis. cmd/omnitalk/main.go now obtains these
knobs via interface assertion instead of depending on the concrete
*ethertalk.PcapPort type.

This keeps transports that don't need bridge configuration (LocalTalk,
LToUDP, future virtual ports) from being forced to implement stubs, and
sets up the eventual move to construction-time PortOptions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ink via constructor

Step 11 of the architectural refactor. The plan called for MacIP to
accept an externally-constructed rawlink.RawLink rather than opening
its own. That shape is already present:

- service/macip/macip.go: Service.New takes ipLink rawlink.RawLink.
- service/macip/etherlink.go: newEtherIPLink takes rawlink.RawLink.
- cmd/omnitalk/main.go: opens the pcap RawLink and injects it.

No macip.Transport interface is warranted: etherlink.go only needs
Read/Write/Close, which RawLink already provides. Verified with
go test ./service/macip/... and go test -tags integration ./service/macip/...

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ContinuePacket

Step 12 of the architectural refactor. Adds pkg/binutil with allocation-
conscious Put/Get helpers for fixed-width big-endian integers and Pascal
strings, plus the canonical Wire interface shape documented in the
package doc comment.

Pilot migration: ASP WriteContinuePacket now implements
MarshalWire/UnmarshalWire/WireSize via binutil. The legacy MarshalData
method is retained as a thin adapter so existing call sites keep
working; follow-up steps migrate remaining models.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…itial counters

Step 13 of the architectural refactor. Adds pkg/telemetry with Counter,
Gauge, and Histogram types backed by expvar (stdlib, zero deps). A
future //go:build otel file can replace the expvar publishers with an
OpenTelemetry backend without touching call sites.

Initial counters wired:
- omnitalk_router_frames_in_total: incremented on every Router.Inbound
- omnitalk_afp_commands_total:     incremented on every AFP HandleCommand
- omnitalk_aarp_probe_retries_total: incremented on every AARP probe retry

Metric declarations live in per-package metrics.go files to avoid
churning existing imports. The Prometheus-style naming convention is
documented in the package doc comment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First slice of Step 14. ATPHeader now implements MarshalWire/
UnmarshalWire/WireSize via pkg/binutil. The legacy Marshal/Unmarshal
methods become thin adapters to keep existing protocol.Packet callers
working unchanged.

Adds a golden hex round-trip test plus short-buffer rejection so any
future regression in the wire layout fails loudly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Second slice of Step 14. DSI Header now implements MarshalWire/
UnmarshalWire/WireSize via pkg/binutil. Legacy Marshal/Unmarshal
methods are retained as adapters so existing call sites in dsi.go
continue to work.

Adds golden hex round-trip + short-buffer rejection tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 16 of the architectural refactor. The AppleDouble v2 sidecar
format (parse/build, magic/version/entry-id constants, SidecarPath)
is generic to any AFP-style server and is no longer buried in
service/afp.

service/afp/appledouble.go shrinks to a thin alias layer that keeps
the historical lowercase identifiers compiling. AFP I/O code
(appledouble_backend.go, desktop_rebuild.go) updates to the exported
field names on appledouble.Parsed.

The AFP-specific ForkMetadataBackend stays in service/afp/ — it
depends on Volume config and disk I/O. A future step may relocate it
to service/afp/fork/ once the package is decomposed (Step 15).

Adds round-trip tests for the new pkg/appledouble package.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 17 of the architectural refactor. The pre-existing FrameSender
interface in port/localtalk/localtalk.go was unused; backends plugged
into Port via a ConfigureSendFrame(closure) call. Adds SetFrameSender
as the interface-shaped counterpart and migrates the LToUDP and
TashTalk backends to it. Each backend now exposes an exported
SendFrame method that satisfies FrameSender, with the legacy
unexported sendFrame retained as the implementation.

ConfigureSendFrame is kept for the virtual/test backends that pass
ad-hoc closures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 20 of the architectural refactor. ARCHITECTURE.md is the entry
point for new contributors: module map, layering rules, core
interfaces, configuration model, logging/telemetry shape, wire codec
convention, timer patterns, AFP architecture, and a glossary.

Reflects the package layout as of Steps 1–17: protocol/ddp lifted
out of appletalk/, pkg/{binutil,appledouble,cnid,logging,telemetry}
in place, MacIP consuming rawlink, BridgeConfigurable extracted, and
LocalTalk backends wired through FrameSender.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add FileSystemFactory + RegisterFS/NewFS in fs.go. local_fs.go and
macgarden_fs.go self-register in init(). newBackendForVolumeConfig
dispatches via the registry instead of switching on FSType, so future
backends register themselves without modifying core AFP.
macgarden_fs.go and its tests now require -tags macgarden. A stub file
under !macgarden registers a factory that returns ErrMacGardenDisabled
so config validation still surfaces a clear message. NormalizeFSType
now consults the registry instead of hardcoding the supported types,
which keeps validation honest across build-tag combinations.

Default builds shed the macgarden HTTP client dependency surface.
The default build now skips modernc.org/sqlite (verified via
'go list -deps ./cmd/omnitalk' before/after — modernc.org/sqlite drops
out without the tag). pkg/cnid/sqlite_stub.go provides no-op
SQLiteStore methods plus error-returning constructors so service/afp's
existing fallback paths ('sqlite init failed; falling back to memory')
route through the in-memory CNID + memory desktop DB stores
transparently.

AFP and macip tagging deferred — those are multi-day refactors
requiring 60+ files tagged plus stub packages for the public API
surface main.go imports. Tracking separately.
Four high-priority sites flagged in the plan now honour service
shutdown instead of blocking on hardcoded timeouts:

* service/asp: Service.Start spins up a lifeCtx; Stop cancels it.
  The three pending.Wait(context.Background()) sites — completeWrite,
  sendTickle, SendAttention — now use s.drainCtx() so background
  ATP transaction releases stop promptly when the server quits.
* service/llap: the CTS-wait select grew <-st.stop and <-s.stop arms
  alongside the timer; time.After replaced with time.NewTimer so the
  timer is properly stopped on early return.
* service/macip dhcp_client: dhcpClient now carries the service stop
  channel; RequestIP returns nil immediately on shutdown rather than
  waiting up to dhcpTimeout.
* service/macip etherlink: resolveMAC's ARP-reply wait selects on
  l.stop alongside the timer; the waiter cleanup logic moved into
  dropARPWaiter so timeout and shutdown paths share it.

Step 24 deferred work: full ctx-as-first-arg propagation through
service.Service / port.Port / atp.Endpoint is a multi-day refactor
with cascading signature changes — out of scope for this pass.
AFPService gains a stop channel and WaitGroup; the desktop-DB rebuild
goroutine kicked off by NewAFPService is now tracked and Stop() waits
for it. Stop also closes any volume FileSystem that exposes Close().

MacGardenFileSystem gains the same lifecycle pair plus a Close()
method. The fire-and-forget HeadContentLength probe loop checks the
stop channel between asset fetches so service shutdown drains cleanly
instead of leaking long-running HTTP requests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds doc.go to packages that previously had no // Package X ... block:
router, port/ethertalk, port/localtalk, service/llap, service/rtmp,
service/zip, service/dsi, service/aep, service/macgarden, and
encoding. Each one-paragraph stub points at the relevant spec/ doc so
go doc / pkg.go.dev surface the right context.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Renames inside the afp package and its sole external caller:
  AFPService        -> Service
  AFPOptions        -> Options
  NewAFPService     -> NewService
  DefaultAFPOptions -> DefaultOptions

Mechanical gofmt -r rewrites; no behaviour change. Both default and
-tags macgarden builds pass; afp + cmd test suites green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the single %v formatting of the AFPService.Stop error slice
with errors.Join + %w so callers can errors.Is/As against any of the
underlying transport or backend Close errors.

Wider audit: the codebase already wraps with %w everywhere a chained
error is reported, and the existing reliance on stdlib sentinels
(fs.ErrPermission, fs.ErrNotExist) covers the categories the plan
suggested adding bespoke AFP sentinels for. No new sentinels needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fuzz both DatagramFromLongHeaderBytes (with and without checksum
verification) and DatagramFromShortHeaderBytes. The harness only
asserts non-panic — any malformed inbound frame must surface as an
error, never as a runtime crash on the router fast path.

Smoke run at -fuzztime=2s exercises ~320k inputs cleanly. Future
protocol/ packages (atp, asp, dsi, afp wire models) get the same
treatment when their wire code lands behind the protocol/* split.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds two jobs to .github/workflows/pr-ci.yml:
- quality: go vet, go test -race, golangci-lint, govulncheck
- build-tags: matrix builds with "" and "macgarden" tags so the
  optional macgarden VFS keeps compiling alongside the default

Adds .golangci.yml enabling errcheck, errorlint, govet, ineffassign,
misspell, staticcheck, unused (errcheck off in tests).

Fixes the one go vet finding the new gate would surface: ASP
SessionManager.Close() was copying a Session containing sync.Mutex
into a local snap before invoking the onClose callback. Callers only
need a pointer (the session is already removed from the map and its
writers are cancelled), so the snap is gone and onClose receives sess
directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Embedded routing builds don't need the IP-over-AppleTalk gateway. Tag
every file in service/macip behind //go:build macip and split the
cmd-layer wiring into a tag-neutral hook interface (macip_hook.go) plus
enabled/disabled implementations. main.go now calls wireMacIP via the
interface and never imports service/macip directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pgodwin and others added 27 commits April 29, 2026 15:40
- golangci-lint: add gocritic + revive; build under -tags=all so afp/macgarden/macip files lint too
- race tests, govulncheck, build matrix all use -tags all (or include sqlite_cnid row)
- new gosec job over the network-input-parsing surface (macip, macgarden)
- Makefile with build/test/test-race/lint/vuln/gosec/fuzz targets for local dev

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- protocol/asp: FuzzParseCommandPacket/Write/OpenSess to round out the
  per-protocol fuzz set (ddp/atp/llap/nbp already had them).
- protocol/atp: BenchmarkHeader{Marshal,Unmarshal,RoundTrip}Wire as a
  zero-alloc baseline so the binutil migration regressions surface.
- encoding/macroman: t.Parallel() on the three pure-function tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NewClient previously fired a GET against macintoshgarden.org during
construction, so even tests that swap in mock RoundTrippers triggered
real network hits before reaching the assertion. Move the prime into a
new Client.Prime() method that production callers invoke explicitly,
and let tests skip it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The plan's netlog->slog migration was always aspirational: only one
production caller (cmd/omnitalk) used pkg/logging directly after
months of work, and pkg/logging/protolog had zero consumers. Living
in the middle was the real cost — service code couldn't tell which
package to reach for and 50+ stdlib log.Printf calls sat in AFP because
"the migration" was supposed to absorb them.

Decision: netlog is the call-site API; pkg/logging is the slog factory
used once at startup. Document the split, replace the lingering
log.Printf calls in service/afp with netlog.Debug (Warn for the panic
recovery), and delete the speculative pkg/logging/protolog package.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
main.go used to write loaded TOML values back into flag pointers so
downstream code could uniformly read flag derefs. The pointers then
lied about their provenance, and a few EtherTalk flags
(desired_network, desired_node) were silently dropped from the TOML
path because the writeback block didn't include them.

Add flagsToConfig to translate flag values into the same appConfig
that loadConfigFromFile produces. main.go now picks one builder, then
reads only from cfg. Renames fileConfig to appConfig and incidentally
fixes the dropped desired_network/desired_node TOML keys.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Separate factory registry, fork metadata abstractions, and error types
into dedicated files so each file has a single concern.

- fs.go keeps the FileSystem interface, factory registry, File interface
- fork_metadata.go: ForkMetadata, ResourceForkInfo, AppleDoubleMode,
  ForkMetadataBackend, CommentBackend
- errors.go: ErrCopySourceReadEOF, NotSupportedError + helpers

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace ASP's runtime type-assertion that pushed quantum into AFP via
SetMaxReadSize with a Transport.MaxReadSize() method that AFP queries
after starting each transport. AFP picks the smallest non-zero limit
and applies it to its filesystems; DSI reports 0 (uncapped TCP stream).

This removes the post-construction mutation pattern, makes the contract
explicit on the Transport interface, and lets AFP own when the limit is
applied instead of trusting transports to call into it at the right time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The MacRoman codec is AppleTalk-adjacent but has no AppleTalk state
and is reusable outside the project — that places it under pkg/ per
the layering rules in ARCHITECTURE.md, not as a top-level package.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both SPCommand and SPWrite repeated the same six-step prologue: cmdblock
size cap → session lookup → activity touch → onActivity → ASP duplicate
filter → handler. Extract it so the per-command handlers shrink to just
the state-machine logic that distinguishes them. Sets up the explicit
write-phase enum and session lifecycle states that follow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace nil-checks on sess.write with a writePhase enum and
beginWrite/endWrite transition methods that assert legal edges. A
second SPWrite arriving before the first resolves now logs an
invariant violation and is rejected with SPErrorParamErr instead of
silently overwriting the in-flight state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a sessionState atomic on Session with stateOpen/stateClosing/
stateClosed and a CAS-based markClosing transition. Inbound handlers
now check isOpen() at entry and reject traffic destined for a session
mid-teardown — previously a CloseSess interleaved with an in-flight
Command could touch state during cancellation. Close serialises through
a single CAS winner so concurrent close paths are safe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace seqMu + writeMu with a single Session.mu. Both protected state
that's only ever touched in microsecond-scale critical sections, and
holding two distinct locks for orthogonal sub-states made the
concurrency surface harder to reason about than it earned. lastActivity
and state stay atomic — the maintenance goroutine reads them without
the mutex.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 5-line CheckDuplicate logic was correct but anonymous, sitting on
Session as three loose fields. Promote it to a seqFilter type with an
accept method and unit test the edge cases — wraparound, first message,
ATP-retransmit-vs-ASP-duplicate distinction — without spinning up a
session. Session.CheckDuplicate stays as the locked entrypoint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 12-method service.Router lumped datagram I/O, the routing-table
index, and the zone-information index into one fat interface that every
service had to receive — so any router refactor rippled into 11
services. Split into:

  DatagramRouter — Route/Reply/PortsList/Zones (every service)
  RouteIndex     — RoutingEntries/Get/Consider/MarkBad/Age (RTMP, ZIP-sending)
  ZoneIndex      — ZonesInNetworkRange/NetworksInZone/AddNetworksToZone (ZIP)

service.Router stays as the union so the Service.Start contract is
unchanged. Internal stash fields (asp, macip) and helpers
(rtmp.makeRoutingTableDatagramData, RoutingTableAgingService) narrow
to the smallest capability they actually use, so future widening of
Router doesn't ripple through them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Carve users/nextSRefNum out of Service into a sessionState type with its
own mutex. Auth-path code (Login, AddUser) no longer contends with fork,
desktop, or volume state on the shared s.mu.

This is the smallest of four substruct extractions; further groups
(forks, desktop, volume) follow incrementally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Carve desktopDBs/dtRefs/nextDTRef out of Service into a desktopState type
with its own RWMutex. The Desktop database call path (FPOpenDT,
FPAddIcon, FPGet/AddAPPL, FPGetComment, etc.) no longer contends with
fork, auth, or volume traffic on the shared s.mu.

Helpers:
  desktop.lookup     — ref → (db, volID, refExists); db may be nil
  desktop.lookupDB   — strict variant; both ref and db must exist
  desktop.openRef    — registers a new DTRefNum, lazily opens the volume DB
  desktop.closeRef   — invalidates a DTRefNum
  desktop.dbForVolume — used by ingest/rebuild paths
  desktop.volumeOf   — DTRefNum → volume id only

Comment handlers use lookup (they fall back to the metadata backend when
no DesktopDB is open). Icon/APPL handlers use lookupDB (they require a
real DB to write into).

Removes the desktopDBForVolumeLocked / "must hold s.mu" helper; ingest
paths now just call s.desktopDBForVolume which is internally synchronised.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Carve forks/nextFork/byteLocks/maxLocks out of Service into a forkState
type with its own RWMutex. Fork I/O (Open/Close/Read/Write/Flush*/
GetForkParms/SetForkParms/ByteRangeLock) is the hottest AFP code path —
every active session touches it on every read or write — so isolating it
from auth, desktop, and volume traffic on a separate lock is the largest
contention win of the four substruct moves.

Helpers:
  forks.register   — installs a handle, returns new fork id
  forks.get        — RLock + map lookup; the hot Read/Write probe
  forks.close      — drops a fork id and evicts its byte-range locks
  forks.snapshot   — copy of all open handles for the volume-wide FPFlush
  forks.lock       — write-lock the whole struct (used by ByteRangeLock,
                     which needs atomicity across handle check + lock-list
                     scan + insertion)

maxReadSize stays on Service: it is set once at Start, read-only afterwards,
not actually fork state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…composition #4)

Final substruct extraction: volumeBackupDate moves to a backupDates type
with its own RWMutex. Volumes / volumeFS / metas / cnidStores stay on
Service since they're write-once-after-installVolumes and need no
synchronisation; the s.mu.RLock guards over those reads were dead weight.

With forkState, sessionState, desktopState, and backupDates each owning
their own lock, Service.mu has no remaining users — removed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two leaks where Stop returned before its goroutines actually exited:

1. macip/etherlink: start() launched readLoop and the gateway-probe via
   raw `go`s with no WaitGroup. close() closed the link but did not wait,
   so resolveMAC could still be in flight after the link was nominally
   torn down. Added a wg, joined it from close().

2. dsi: the accept loop spawned per-conn handlers tracked in s.wg, but
   handleConn blocks in io.ReadFull. Closing the listener unblocks
   Accept, not the in-flight reads, so Stop deadlocked on s.wg.Wait.
   Tracked accepted conns in a map; Stop now closes them all so handlers
   return EOF and exit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Directed-transmit's CTS wait selected on s.stop and st.stop but not
s.ctx.Done(). Stop closes both, so the existing path was correct, but
callers that cancel the parent ctx without going through Stop (graceful
SIGINT in main) had no way to unblock an in-flight CTS retry.

Pre-arm s.ctx in New() with a never-cancelled background ctx so
handlers reached before Start (test paths that exercise transmit
directly) don't dereference a nil ctx; Start replaces it with the real
ctx and cancels the placeholder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three packages were missing the leading "Package X ..." doc comment so
go doc and pkg.go.dev surfaced no overview. Added a doc.go in each
naming the role and pointing at the spec / where related code lives.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Switch transaction.go to import protocol/atp as patp and reference its
exported header, control bits, sizes, and limits rather than
package-local duplicates, aligning the runtime layer with the
canonical wire-format package.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce BUILD_VARIANT (all|router) in build and package scripts so a
single run can produce both the full binary and a slim router-only
binary built without AFP/MacIP tags. Release workflow now matrixes
each OS over both variants and publishes them as separate archives
(omnitalk-* and omnitalk-router-*).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move cnid_backend, use_decomposed_names, and the default appledouble_mode
out of the per-volume table and into [AFP] where they actually live;
add the missing desktop_backend and persistent_volume_ids keys; document
the per-volume password field. Drops the stale fork_backend reference
in the sidecar metadata notes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four client tests reach macintoshgarden.org and fail on offline CI
runners. Add a requireLiveTests helper that skips them unless
OMNITALK_LIVE_TESTS=1 is set, so PR CI stays green while the tests
remain runnable locally on demand.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
golangci-lint-action v6 pins golangci-lint to the v1 line (1.64.8),
which rejects our v2-style .golangci.yml ("linters.default" and the
top-level "version" key). v8 supports golangci-lint v2. Drop the
deprecated --timeout flag; the timeout is already set in config.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@pgodwin
Copy link
Copy Markdown
Contributor Author

pgodwin commented May 1, 2026

Linting issues - will merge.

@pgodwin pgodwin merged commit aaa263f into main May 1, 2026
11 of 12 checks passed
@pgodwin pgodwin deleted the feat-cleanup branch May 1, 2026 04:51
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.

1 participant