main ece7682

broker_daemon_start.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"os"
8	"path/filepath"
9	"syscall"
10
11	"github.com/bnema/vev/internal/adapters/daemonmux"
12	"github.com/bnema/vev/internal/adapters/ipc"
13	"github.com/bnema/vev/internal/ports"
14	"github.com/bnema/vev/internal/usecase/broker"
15)
16
17// Broker-owned daemon start.
18//
19// The broker is the only component that decides whether a target daemon may be
20// started. Its resolved dial target already carries the closed start
21// authorization (ports.BrokerDaemonStartMode) together with the policy and the
22// endpoint fence, and this file is the single place where that authorization
23// reaches transport mechanics:
24//
25//   - BrokerDaemonExistingOnly dials the private daemonmux carriage and nothing
26//     else. It never probes the lifecycle lock, never takes the spawn lock,
27//     never launches a process, and never dials a session: an existing-only
28//     acquisition is exactly one carriage dial.
29//   - BrokerDaemonStartIfNeeded dials the same carriage first. A successful dial
30//     is the whole result — an already running daemon is never restarted. Only
31//     when the dial proves the daemon is absent does the start proceed, and only
32//     when the resolved policy explicitly authorizes launching. The start itself
33//     is the existing election in spawn.go: lifecycle availability, one elected
34//     spawner under the shared spawn lock, then a bounded redial of the same
35//     carriage under the same budget.
36//
37// The result handed to the pool is always daemonmux: no session transport is
38// ever produced here, so no caller can fall back to the direct session dial the
39// broker replaced. Readiness is proven by the carriage the pool then
40// authenticates with the daemonmux physical preamble, never by a session dial,
41// a session control stream, or a Hello.
42//
43// Refusals are explicit and typed. A daemon that is absent may only be started
44// under an authorizing policy; a carriage that exists but refuses the peer, the
45// path, or the credentials is never "fixed" by spawning a second daemon, and a
46// start that leaves the carriage still undialable is reported rather than
47// retried with a session dial. Identity, incarnation, policy, and version are
48// verified by the daemonmux preamble after this function returns, so those
49// failures can never trigger a spawn either.
50
51// The start decision itself (mode, launch authority, local-only target) is
52// owned by broker.ClassifyDaemonDialFailure, broker.AuthorizeDaemonLaunch, and
53// broker.RequireLocalDaemonTarget.
54// This file keeps only the mechanics: carriage path validation, absence
55// classification of a dial error, and the spawn election.
56
57// brokerDaemonStarter owns the three facts a daemon start needs: the private
58// daemonmux carriage dial, the detached daemon launcher, and the exact carriage
59// the launched daemon will publish. All three are seams so tests exercise the
60// real election, policy, and refusal rules without binding a socket or
61// re-executing a daemon.
62type brokerDaemonStarter struct {
63	dial    func(ctx context.Context, carriage string) (daemonmux.RawFramedTransport, error)
64	spawn   spawnFunc
65	backoff backoffConfig
66	// published returns the private daemonmux carriage the spawned daemon binds.
67	// Nil means the production endpoint of this process.
68	published func() string
69}
70
71// localDaemonStarter is the production starter: the real private Unix daemonmux
72// carriage, the real double-fork daemon launcher, and the exact endpoint that
73// launcher publishes. It is the broker process' starter and a remote mux
74// helper's starter alike, because both start the daemon published on their own
75// machine by re-executing their own binary. A test replaces it with scripted
76// mechanics and restores it immediately; it is package state for the same reason
77// defaultBackoff is, and no test mutates it in parallel.
78var localDaemonStarter = brokerDaemonStarter{dial: dialLocalDaemonCarriage, spawn: realSpawn}
79
80// effectiveBackoff returns the starter's explicit backoff, or the production
81// default read at call time, so a test that narrows defaultBackoff still governs
82// the daemon start.
83func (s brokerDaemonStarter) effectiveBackoff() backoffConfig {
84	if s.backoff.total > 0 {
85		return s.backoff
86	}
87	return defaultBackoff
88}
89
90// effectivePublished returns the private daemonmux carriage the spawned daemon
91// binds: the starter's explicit endpoint, or this process' own production
92// daemon endpoint. The daemon launcher re-executes this binary, which always
93// binds daemonmux.SocketPath(ipc.SocketDir()), so that is the only carriage a
94// start can ever produce.
95func (s brokerDaemonStarter) effectivePublished() string {
96	if s.published != nil {
97		return s.published()
98	}
99	return daemonmux.SocketPath(ipc.SocketDir())
100}
101
102// dialLocalDaemonCarriage dials exactly the private daemonmux carriage at
103// carriage, verifying that the connected peer is the current effective user. It
104// is deliberately the mux carriage only: the daemon's public session socket is
105// never dialed here.
106func dialLocalDaemonCarriage(ctx context.Context, carriage string) (daemonmux.RawFramedTransport, error) {
107	return ipc.DialMuxContext(ctx, carriage)
108}
109
110// dialBrokerLocalDaemon dials the daemon published on this machine for one
111// resolved local target. carriage is the private daemonmux carriage of the
112// broker-owned local route: that route is its only source, and this function
113// never derives, guesses, or accepts a carriage of its own.
114//
115// A registered endpoint is refused even when it is provisioned on a local Unix
116// carriage: only the broker-owned local authority names the daemon this
117// process can start, so a configured host can never be spawned by mistake.
118func dialBrokerLocalDaemon(ctx context.Context, carriage string, target ports.BrokerDialTarget) (daemonmux.RawFramedTransport, error) {
119	if err := broker.RequireLocalDaemonTarget(target); err != nil {
120		return nil, err
121	}
122	return dialBrokerDaemonCarriage(ctx, carriage, target)
123}
124
125// dialBrokerDaemonCarriage is the shared carriage dial and start. The target's
126// start authorization decides the whole behavior. ExistingOnly performs exactly
127// one carriage dial. StartIfNeeded performs the same dial first, returns the
128// daemon it found, and only starts a daemon when the dial proves absence and the
129// resolved policy authorizes launching; otherwise it reports the refusal without
130// touching any process.
131//
132// A start is only ever possible for the one daemon this process can actually
133// produce: the launcher re-executes this binary, which binds its private
134// daemonmux carriage at daemonmux.SocketPath(ipc.SocketDir()) and holds its
135// lifecycle and spawn locks in that same runtime directory. A carriage that is
136// anything else is dialed and, if absent, reported — never replaced by a daemon
137// this process did not start.
138func dialBrokerDaemonCarriage(ctx context.Context, carriage string, target ports.BrokerDialTarget) (daemonmux.RawFramedTransport, error) {
139	if err := target.Validate(); err != nil {
140		return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: err}
141	}
142	if err := validateDaemonCarriage(carriage); err != nil {
143		return nil, err
144	}
145	starter := localDaemonStarter
146	if starter.dial == nil || starter.spawn == nil {
147		return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: errors.New("daemon starter is incomplete")}
148	}
149	raw, err := starter.dial(ctx, carriage)
150	if err == nil {
151		return raw, nil
152	}
153	// The broker decides; nothing below runs unless it authorizes a start.
154	// The carriage check sits between its two steps, so a foreign carriage is
155	// reported as incompatible before any policy refusal.
156	if err := broker.ClassifyDaemonDialFailure(target, err, daemonCarriageAbsence(err)); err != nil {
157		return nil, err
158	}
159	if err := validateDaemonCarriageStart(carriage, starter); err != nil {
160		return nil, err
161	}
162	if err := broker.AuthorizeDaemonLaunch(target); err != nil {
163		return nil, err
164	}
165	published := starter.effectivePublished()
166	return ensureMuxDaemon(ctx, filepath.Dir(published), carriage, starter.dial, starter.spawn, starter.effectiveBackoff())
167}
168
169// validateDaemonCarriage refuses a carriage that cannot be a provisioned private
170// daemonmux endpoint: it must be an absolute, cleaned path. The daemon launcher
171// re-executes this binary to start one specific daemon, so a relative, unclean,
172// or empty path can never trigger a spawn.
173func validateDaemonCarriage(carriage string) error {
174	if carriage == "" {
175		return ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: errors.New("daemon carriage path is empty")}
176	}
177	if !filepath.IsAbs(carriage) || filepath.Clean(carriage) != carriage {
178		return ports.BrokerError{
179			Code:  ports.BrokerErrorIncompatible,
180			Cause: fmt.Errorf("daemon carriage %q is not an absolute cleaned path", carriage),
181		}
182	}
183	return nil
184}
185
186// validateDaemonCarriageStart refuses starting a daemon for any carriage other
187// than the one the launched daemon will actually publish. The launcher can only
188// ever produce the daemon bound to that exact private endpoint, so starting one
189// for a foreign carriage would spawn a process the caller could never reach.
190func validateDaemonCarriageStart(carriage string, starter brokerDaemonStarter) error {
191	if want := starter.effectivePublished(); carriage != want {
192		return ports.BrokerError{
193			Code:  ports.BrokerErrorIncompatible,
194			Cause: fmt.Errorf("refusing to start a daemon for carriage %q: this process publishes %q", carriage, want),
195		}
196	}
197	return nil
198}
199
200// daemonCarriageAbsence reports whether a failed carriage dial means the daemon
201// is simply not there, so starting one is the only remaining option. It is
202// deliberately fail-closed: a cancelled attempt, an invalid or foreign path, a
203// rejected peer, a permission failure, and every unclassified error report
204// false, so only proven absence can authorize a spawn.
205func daemonCarriageAbsence(err error) bool {
206	switch {
207	case err == nil:
208		return false
209	case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
210		return false
211	case errors.Is(err, ipc.ErrMuxPath), errors.Is(err, ipc.ErrMuxForeignPath),
212		errors.Is(err, ipc.ErrMuxPeerRejected), errors.Is(err, ipc.ErrMuxUnsupported):
213		return false
214	case errors.Is(err, os.ErrPermission):
215		return false
216	case errors.Is(err, os.ErrNotExist), errors.Is(err, syscall.ENOENT),
217		errors.Is(err, syscall.ECONNREFUSED), errors.Is(err, syscall.EWOULDBLOCK):
218		return true
219	default:
220		return false
221	}
222}
223
224// brokerDaemonStartArgvFlag renders the closed start authorization as the exact
225// helper argument value. An unknown mode is refused, never defaulted: a helper
226// that cannot read its authorization must not run with a permissive one.
227func brokerDaemonStartArgvFlag(mode ports.BrokerDaemonStartMode) (string, error) {
228	switch mode {
229	case ports.BrokerDaemonExistingOnly:
230		return "existing-only", nil
231	case ports.BrokerDaemonStartIfNeeded:
232		return "if-needed", nil
233	default:
234		return "", fmt.Errorf("invalid broker daemon start mode %s", mode)
235	}
236}
237
238// parseBrokerDaemonStartArgvFlag is the strict inverse of
239// brokerDaemonStartArgvFlag. An unknown, empty, or differently spelled value is
240// refused; a helper never maps an unrecognized word onto a start authorization.
241func parseBrokerDaemonStartArgvFlag(value string) (ports.BrokerDaemonStartMode, error) {
242	switch value {
243	case "existing-only":
244		return ports.BrokerDaemonExistingOnly, nil
245	case "if-needed":
246		return ports.BrokerDaemonStartIfNeeded, nil
247	default:
248		return 0, fmt.Errorf("unknown daemon start mode %q (want existing-only or if-needed)", value)
249	}
250}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close