instance_launch.go RawHistoryFolder

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}

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 8

Tags 42

↑↓ moveenter openesc close