-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathServerProbe.qml
More file actions
164 lines (149 loc) · 7.47 KB
/
Copy pathServerProbe.qml
File metadata and controls
164 lines (149 loc) · 7.47 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
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import "VncModel.js" as VncModel
// The one wayvnc listing every bar reads, and the only place a pgrep is started.
//
// A bar surface exists per monitor, so BarWidget.qml exists once per output —
// and the state it shows (which outputs are served, which wayvnc nobody here
// started is holding a port) is the same answer for all of them. Kept per
// widget, one listing was still one process per monitor every cycle — three on
// this desk, more on a bigger one, all asking the same question. Kept here, it
// is one process for the whole shell however many monitors are plugged in, and
// every widget binds to the same answer, so no two bars can disagree about what
// is running.
//
// A file-level singleton needs the qmldir next to it to be one: without the
// `singleton` line there the name resolves to the component type rather than to
// an instance, every property read off it is undefined, and the widget's first
// binding on one (servedOutputs.indexOf) throws. That is the loud failure, not
// the quiet one — nothing shares anything and the panel is broken — so the
// qmldir is load-bearing rather than tidy.
Singleton {
id: root
// Every output with a server of the exact shape VncModel.startCommand
// builds, and the command lines of every other wayvnc — see
// VncModel.probeState for why the second list is reported and never acted on.
property var servedOutputs: []
property var externalServers: []
// The bars whose panel is open, held by identity rather than counted, so a
// widget that says so twice is held once and a release for a widget that is
// not holding is nothing. Both happen: the panel's opened state and the
// widget's destruction are separate events, and a monitor unplugged with the
// panel open produces both.
property var openPanels: []
// Every widget currently reading this, by the same identity rule. A reload
// (shell.qml's rescan clears the component cache and recompiles BarWidget.qml)
// destroys every widget and builds a fresh singleton for the new ones, while
// the engine's singleton table keeps this one alive: same object, no readers,
// and a served list frozen at whatever the last listing said. Emptying this is
// what tells such an instance to go quiet, instead of leaving one more pgrep
// every 5s behind after every reload.
property var attached: []
readonly property bool polling: VncModel.shouldPoll(root.attached.length,
root.openPanels.length, root.servedOutputs, root.externalServers)
// Whether an ask arrived while a listing was already in flight — see refresh.
property bool pending: false
// Re-arming a Process by flipping running off and on inside one event-loop
// turn can coalesce into no run at all, so only ever arm an idle one.
//
// An ask that lands on a busy Process is remembered rather than dropped,
// because the answer already on its way is not the answer that was asked for:
// the pgrep in flight was launched before whatever prompted this call — the
// settle 500ms after a toggle, most of the time — so it cannot have seen what
// just changed. Dropping it used to be repaired by the next poll a few seconds
// later; with polling gated on what the listing says, the dropped answer is
// exactly the one that would have turned the poll back on, and the bar would
// sit there saying nothing is shared until somebody opened a panel.
//
// The flag is one, not a queue, so a crowd of asks in one turn costs one extra
// listing between them however many there were: the bars all refreshing at
// startup is two pgreps, not one per monitor.
function refresh() {
if (probe.running) { root.pending = true; return }
probe.running = true
}
// Probes as it takes the hold rather than waiting for the timer, because the
// panel that just opened is drawn from this state and the timer says nothing
// about when it last spoke: idle, it was not running at all; polling, it is
// somewhere mid-interval and the panel would open showing an answer up to 5s
// old. This is also why the timer itself does not probe as it starts.
function holdOpen(owner) {
if (!owner || root.openPanels.indexOf(owner) !== -1) return
root.openPanels = root.openPanels.concat([owner])
root.refresh()
}
function releaseOpen(owner) {
root.openPanels = without(root.openPanels, owner)
}
// Taken for as long as a widget exists, which is a different lifetime from
// the hold above: a panel is open for a moment, a widget for as long as its
// bar. Idempotent the same way, and for the same reason — a widget destroyed
// with its panel open gives back both, in one handler.
function attach(owner) {
if (!owner || root.attached.indexOf(owner) !== -1) return
root.attached = root.attached.concat([owner])
}
function detach(owner) {
root.attached = without(root.attached, owner)
}
// Removing by identity, so handing back something that is not held is nothing
// rather than a decrement somebody else owned.
function without(list, owner) {
var index = list.indexOf(owner)
if (index === -1) return list
var next = list.slice()
next.splice(index, 1)
return next
}
// One listing for every output. pgrep exits 1 and prints nothing when no
// server is up, which parses to the empty list — the same answer, so the exit
// code needs no separate handling.
Process {
id: probe
command: VncModel.PROBE_ARGS
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var state = VncModel.probeState(text)
root.servedOutputs = state.outputs
root.externalServers = state.external
}
}
// The ask that arrived mid-flight, run now that there is something idle to
// run it on. Through callLater rather than straight away, because this is
// the turn the Process went idle in and arming it here is the coalescing
// the arming rule exists to avoid.
onRunningChanged: {
if (probe.running || !root.pending) return
root.pending = false
Qt.callLater(root.refresh)
}
}
// Runs only while VncModel.shouldPoll says the answer is worth having, and
// does not probe as it starts: every way it can start has just produced a
// listing anyway. Turned on by an open panel, holdOpen has already asked;
// turned on by something appearing in the listing, the listing that turned it
// on is the fresh one. triggeredOnStart here was a second pgrep a moment after
// the first, every time.
//
// The trade-off, stated plainly: with every panel closed and nothing served,
// nothing here is looking. A wayvnc somebody starts by hand in that state is
// not noticed — no bar icon reacts to it and externalRunning stays false, so
// the serve switches stay live for ports that are already taken — until a
// panel is opened, which probes at once and puts every bar right. The serve
// IPC verbs open no panel and answer synchronously, so a scripted `serve on`
// in that state is decided on a listing of no particular age; see the README
// for what that costs. The cost of the alternative is a pgrep every 5s forever
// on a machine that is sharing nothing, which is what this replaced.
Timer {
interval: 5000
running: root.polling
repeat: true
onTriggered: root.refresh()
}
// Answered as soon as this exists, so a shell restarted while servers are up
// finds them — and, finding them, starts polling on its own.
Component.onCompleted: root.refresh()
}