v0.3.0 99dc266

handshake.go RawHistoryFolder

1package client
2
3import (
4	"context"
5	"errors"
6	"sync"
7	"time"
8
9	"github.com/bnema/vev/internal/ports"
10)
11
12var errHandshakeTimeout = errors.New("handshake timed out")
13
14type systemClock struct{}
15type systemTimer struct{ *time.Timer }
16
17func (systemClock) Now() time.Time { return time.Now() }
18func (systemClock) NewTimer(delay time.Duration) ports.Timer {
19	return systemTimer{Timer: time.NewTimer(delay)}
20}
21func (t systemTimer) C() <-chan time.Time        { return t.Timer.C }
22func (t systemTimer) Reset(d time.Duration) bool { return t.Timer.Reset(d) }
23func (t systemTimer) Stop() bool                 { return t.Timer.Stop() }
24
25// newHandshakeContext owns one deadline for the complete outbound handshake.
26// The caller must stop it before entering the long-lived connection loop.
27func newHandshakeContext(parent context.Context, clock ports.Clock) (context.Context, <-chan struct{}, func()) {
28	if parent == nil {
29		parent = context.Background()
30	}
31	ctx, cancel := context.WithCancel(parent)
32	timedOut := make(chan struct{})
33	if clock == nil {
34		clock = systemClock{}
35	}
36	timer := clock.NewTimer(ports.HandshakeTimeout)
37	if timer == nil {
38		timer = systemClock{}.NewTimer(ports.HandshakeTimeout)
39	}
40	stop := make(chan struct{})
41	done := make(chan struct{})
42	go func() {
43		defer close(done)
44		select {
45		case <-timer.C():
46			close(timedOut)
47			cancel()
48		case <-stop:
49		}
50	}()
51	var once sync.Once
52	finish := func() {
53		once.Do(func() {
54			close(stop)
55			<-done
56			timer.Stop()
57			cancel()
58		})
59	}
60	return ctx, timedOut, finish
61}
62
63// watchHandshakeTransport closes a transport when the handshake context ends.
64// Transport.Close is required to interrupt blocked Send and Recv operations.
65func watchHandshakeTransport(ctx context.Context, transport ports.Transport) func() {
66	stop := make(chan struct{})
67	done := make(chan struct{})
68	go func() {
69		defer close(done)
70		select {
71		case <-ctx.Done():
72			_ = transport.Close()
73		case <-stop:
74		}
75	}()
76	var once sync.Once
77	return func() {
78		once.Do(func() {
79			close(stop)
80			<-done
81		})
82	}
83}
84
85func boundedHandshakeOperation(ctx context.Context, transport ports.Transport, operation func() error) error {
86	if err := ctx.Err(); err != nil {
87		_ = transport.Close()
88		return err
89	}
90	completed := make(chan error, 1)
91	go func() { completed <- operation() }()
92	select {
93	case err := <-completed:
94		if ctxErr := ctx.Err(); ctxErr != nil {
95			_ = transport.Close()
96			return ctxErr
97		}
98		return err
99	case <-ctx.Done():
100		_ = transport.Close()
101		// The result channel is buffered, so the operation can publish its
102		// completion after Close without keeping this cancellation path stuck.
103		return ctx.Err()
104	}
105}
106
107func boundedDial(ctx context.Context, dialer ports.Dialer) (ports.Transport, error) {
108	// The dial context only bounds startup. A successful carriage outlives the
109	// handshake context; canceling that context after Welcome must not kill SSH.
110	dialCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
111	result := make(chan struct {
112		transport ports.Transport
113		err       error
114	}, 1)
115	go func() {
116		transport, err := dialer.Dial(dialCtx)
117		if err != nil {
118			cancel()
119		} else if ctx.Err() != nil && transport != nil {
120			_ = transport.Close()
121		}
122		result <- struct {
123			transport ports.Transport
124			err       error
125		}{transport: transport, err: err}
126	}()
127	select {
128	case result := <-result:
129		return result.transport, result.err
130	case <-ctx.Done():
131		cancel()
132		return nil, ctx.Err()
133	}
134}
135
136func handshakeContextError(parent context.Context, timedOut <-chan struct{}, fallback error) error {
137	select {
138	case <-timedOut:
139		return errors.Join(errHandshakeTimeout, context.DeadlineExceeded)
140	default:
141	}
142	if err := parent.Err(); err != nil {
143		return err
144	}
145	return fallback
146}

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