1package bootstrap
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "time"
11
12 "golang.org/x/sys/unix"
13
14 "github.com/bnema/dumber/internal/application/port"
15 "github.com/bnema/dumber/internal/infrastructure/runtimeprofile"
16)
17
18const (
19 instanceRuntimeDirPerm = 0o700
20 instanceLockFilePerm = 0o600
21 instanceLockRetry = 25 * time.Millisecond
22 instanceElectionSlice = 150 * time.Millisecond
23)
24
25// errInstanceNamespaceContended marks a lease acquisition that failed because
26// another process holds the namespace lock. It is the only acquisition failure
27// callers may retry; permanent failures (mkdir/open/flock) must surface at once.
28var errInstanceNamespaceContended = errors.New("named instance namespace contended")
29
30type namespaceLease struct{ file *os.File }
31
32func (l *namespaceLease) Close() error {
33 if l == nil || l.file == nil {
34 return nil
35 }
36 err := unix.Flock(int(l.file.Fd()), unix.LOCK_UN)
37 closeErr := l.file.Close()
38 l.file = nil
39 return errors.Join(err, closeErr)
40}
41
42func acquireInstanceNamespace(ctx context.Context, profile runtimeprofile.Profile) (io.Closer, error) {
43 if err := os.MkdirAll(profile.InstanceRoot, instanceRuntimeDirPerm); err != nil {
44 return nil, fmt.Errorf("create named instance runtime directory: %w", err)
45 }
46 path := filepath.Join(profile.InstanceRoot, "namespace.lock")
47 file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, instanceLockFilePerm)
48 if err != nil {
49 return nil, fmt.Errorf("open named instance namespace lock: %w", err)
50 }
51 for {
52 if err = unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB); err == nil {
53 return &namespaceLease{file: file}, nil
54 }
55 if !errors.Is(err, unix.EWOULDBLOCK) {
56 _ = file.Close()
57 return nil, fmt.Errorf("lock named instance namespace: %w", err)
58 }
59 select {
60 case <-ctx.Done():
61 _ = file.Close()
62 return nil, fmt.Errorf("wait for named instance namespace: %w: %w", errInstanceNamespaceContended, ctx.Err())
63 case <-time.After(instanceLockRetry):
64 }
65 }
66}
67
68// DeliverOrRunWithInstanceRelay retries delivery while another process owns the
69// namespace, and only the process that acquires the lease starts the host.
70func DeliverOrRunWithInstanceRelay(
71 ctx context.Context,
72 profile runtimeprofile.Profile,
73 relay port.BrowserLaunchRelay,
74 name, url string,
75 freshWindow bool,
76 run func() int,
77) (int, error) {
78 deliver := relay.DeliverOpenExternalURL
79 if freshWindow {
80 deliver = relay.DeliverOpenFreshWindow
81 } else if name != "" {
82 instanceRelay, ok := relay.(port.BrowserInstanceLaunchRelay)
83 if !ok {
84 return 1, fmt.Errorf("named instance requires an instance launch relay")
85 }
86 deliver = func(ctx context.Context, url string) (bool, error) {
87 return instanceRelay.DeliverOpenInstance(ctx, name, url)
88 }
89 }
90 // Normalize before any acquisition attempt so a cold default profile locks
91 // the shared IPC runtime directory instead of retrying a permanent mkdir/open
92 // failure on an empty path.
93 if profile.InstanceRoot == "" {
94 profile.InstanceRoot = profile.IPC.RuntimeDir
95 }
96 for {
97 deliveryCtx, cancel := context.WithTimeout(ctx, instanceElectionSlice)
98 delivered, err := deliver(deliveryCtx, url)
99 cancel()
100 if delivered {
101 // An unconfirmed ACK still means ownership may have transferred; retrying
102 // the same request would rely on relay request-ID deduplication.
103 return 0, err
104 }
105
106 acquireCtx, cancel := context.WithTimeout(ctx, instanceElectionSlice)
107 lease, acquireErr := acquireInstanceNamespace(acquireCtx, profile)
108 cancel()
109 if acquireErr == nil {
110 return runWithInstanceLease(profile, relay, lease, run)
111 }
112 if !errors.Is(acquireErr, errInstanceNamespaceContended) {
113 // Permanent failure (mkdir, open, unexpected flock error): retrying
114 // cannot succeed, so surface it instead of spinning.
115 return 1, fmt.Errorf("elect named instance host: %w", acquireErr)
116 }
117 if ctx.Err() != nil {
118 return 1, fmt.Errorf("elect named instance host: %w", ctx.Err())
119 }
120 }
121}
122
123// RunWithInstanceRelay owns the namespace lease through full GUI and engine teardown.
124func RunWithInstanceRelay(ctx context.Context, profile runtimeprofile.Profile, relay port.BrowserLaunchRelay, run func() int) (int, error) {
125 lease, err := acquireInstanceNamespace(ctx, profile)
126 if err != nil {
127 return 1, err
128 }
129 return runWithInstanceLease(profile, relay, lease, run)
130}
131
132func runWithInstanceLease(profile runtimeprofile.Profile, relay port.BrowserLaunchRelay, lease io.Closer, run func() int) (int, error) {
133 preparer, ok := relay.(port.BrowserLaunchRelayPreparer)
134 if !ok {
135 return 1, fmt.Errorf("named instance requires a preparable browser launch relay")
136 }
137 if profile.InstanceRoot == "" {
138 profile.InstanceRoot = profile.IPC.RuntimeDir
139 }
140 defer func() { _ = lease.Close() }()
141 if err := preparer.Prepare(lease); err != nil {
142 return 1, fmt.Errorf(
143 "prepare named instance IPC; remove %q only after verifying no Dumber instance is running: %w",
144 profile.IPC.BrowserLaunchSocket,
145 err,
146 )
147 }
148 return run(), nil
149}