-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsingleinstance.go
More file actions
170 lines (152 loc) · 5.85 KB
/
Copy pathsingleinstance.go
File metadata and controls
170 lines (152 loc) · 5.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package main
import (
"hash/fnv"
"io"
"log"
"net"
"strconv"
"strings"
"time"
)
// Single-instance guard.
//
// Wails v3 has no built-in single-instance lock (unlike Electron's
// requestSingleInstanceLock), so a process-level guard is implemented here on
// top of the standard net package. The mechanism is the same on every platform:
//
// 1. The first instance binds a local TCP listener on 127.0.0.1:<port>. The
// bind itself is the lock; whoever owns the port is the primary instance.
// 2. A second instance fails to bind the same port, dials the primary instead,
// writes a "show" instruction over the connection, and exits immediately.
// 3. The primary's accept loop receives the instruction and reveals its window.
//
// TCP is chosen over Unix domain sockets because the latter need platform
// specific path/permission handling (and a TCP fallback on Windows anyway),
// whereas plain loopback TCP behaves identically across macOS, Windows and
// Linux with no extra dependencies.
// InstanceMode reports whether the current process is the primary instance.
type InstanceMode int
const (
// InstancePrimary means no other instance was detected; the caller owns the
// lock and should keep running.
InstancePrimary InstanceMode = iota
// InstanceSecond means another instance already owns the lock. The caller
// has already signalled it and should exit.
InstanceSecond
)
const (
// instanceShowCmd is the single IPC instruction a second instance sends to
// ask the primary to surface its window.
instanceShowCmd = "show\n"
// instanceDialTimeout bounds how long a second instance waits while trying
// to reach the primary before giving up.
instanceDialTimeout = 2 * time.Second
)
// instanceAddress returns the loopback host:port shared by every instance of
// this build. The port is derived from appName so different apps do not collide
// and is mapped into the high dynamic range (49152–65535), which is least
// likely to overlap with registered services.
func instanceAddress(appName string) string {
h := fnv.New32a()
_, _ = h.Write([]byte(appName))
const (
dynamicStart = 49152
dynamicSpan = 65535 - dynamicStart + 1
)
port := dynamicStart + int(h.Sum32()%dynamicSpan)
return net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
}
// DetectInstance decides whether the current process may run.
//
// It must be called as early as possible in main, before the window or app are
// constructed. When it returns InstanceSecond the caller should exit at once —
// the primary has already been asked to surface its window. When it returns
// InstancePrimary the caller owns the lock and should later call ServeInstance
// to begin accepting show requests once its window is ready.
//
// Any failure to detect (for example the loopback interface being unavailable)
// is logged and treated as InstancePrimary so a detection glitch can never
// block the application from starting; the worst case degrades to the previous
// multi-instance behaviour.
func DetectInstance(appName string) InstanceMode {
addr := instanceAddress(appName)
listener, err := net.Listen("tcp", addr)
if err == nil {
// Nobody is listening yet — we are the primary. Hold the listener for
// ServeInstance to take over.
primaryListener = listener
return InstancePrimary
}
// The port is taken. Either a previous instance owns it or an unrelated
// process happens to occupy the same derived port. Try to contact it: if it
// speaks our protocol, treat this as a second instance; if not, fall back to
// running as primary (multi-instance) rather than refusing to start.
if signalPrimary(addr) {
return InstanceSecond
}
log.Printf("single-instance: port %s busy by unknown process; continuing as primary", addr)
primaryListener = nil
return InstancePrimary
}
// primaryListener holds the listener captured by DetectInstance so ServeInstance
// can reuse it instead of re-binding. It is nil when DetectInstance did not win
// the lock.
var primaryListener net.Listener
// signalPrimary connects to the primary and asks it to show its window. It
// reports whether a primary instance acknowledged the request.
func signalPrimary(addr string) bool {
conn, err := net.DialTimeout("tcp", addr, instanceDialTimeout)
if err != nil {
return false
}
defer conn.Close()
if _, err := conn.Write([]byte(instanceShowCmd)); err != nil {
return false
}
// Wait briefly for the primary to close the connection, which signals it
// received and handled the command.
_ = conn.SetReadDeadline(time.Now().Add(instanceDialTimeout))
_, _ = io.Copy(io.Discard, conn)
return true
}
// ServeInstance begins accepting single-instance show requests on the lock
// captured by DetectInstance. onShow is invoked (from a goroutine) each time a
// second instance signals; call it after the application window exists so onShow
// can safely reveal it. It is a no-op if DetectInstance did not win the lock.
func ServeInstance(onShow func()) {
listener := primaryListener
if listener == nil {
return
}
go serveInstanceLoop(listener, onShow)
}
func serveInstanceLoop(listener net.Listener, onShow func()) {
for {
conn, err := listener.Accept()
if err != nil {
// Listener closed (process is shutting down); stop the loop.
return
}
go handleInstanceConn(conn, onShow)
}
}
func handleInstanceConn(conn net.Conn, onShow func()) {
defer conn.Close()
// Read the command. A small bounded buffer is enough: we only ever expect
// the single "show\n" line, and we cap the read so a misbehaving peer cannot
// stream unbounded data.
buf := make([]byte, 64)
_ = conn.SetReadDeadline(time.Now().Add(instanceDialTimeout))
n, err := conn.Read(buf)
if err != nil && n == 0 {
return
}
cmd := strings.TrimSpace(string(buf[:n]))
// Close promptly so the signalling peer's drain returns without delay; we do
// not need to consume the rest of its output.
if cmd == "show" {
if onShow != nil {
onShow()
}
}
}