Repository navigation
Expand file tree
/
Copy pathruntime.zig
More file actions
276 lines (252 loc) · 12.5 KB
/
Copy pathruntime.zig
File metadata and controls
276 lines (252 loc) · 12.5 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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
//! The terminal has one owner. Workers only enqueue output and request answers.
const std = @import("std");
const sgr = @import("../ui/style.zig");
const layout = @import("../ui/layout.zig");
const Agent = @import("../agent/mod.zig").Agent;
const Screen = @import("../ui/screen.zig").Screen;
const menu = @import("../ui/menu.zig");
test "submit relinquishes ownership before tick fails to launch" {
var vtable = std.testing.io.vtable.*;
vtable.concurrent = std.Io.failingConcurrent;
var agent: Agent = undefined;
agent.allocator = std.testing.allocator;
agent.io = .{ .userdata = std.testing.io.userdata, .vtable = &vtable };
agent.interrupted = .init(false);
var runtime: Runtime = .{ .agent = &agent, .screen = undefined };
defer runtime.queue.deinit(agent.allocator);
try std.testing.expectError(error.ConcurrencyUnavailable, runtime.submit("queued"));
try std.testing.expectEqual(@as(usize, 0), runtime.queue.items.len);
try std.testing.expect(runtime.future == null);
}
pub const Runtime = struct {
agent: *Agent,
screen: *Screen,
future: ?std.Io.Future(void) = null,
done: std.atomic.Value(bool) = .init(false),
queue: std.ArrayList([]u8) = .empty,
/// The form the worker is waiting on. The worker publishes its questions
/// under the mutex and sleeps; the main thread draws them, because it is the
/// one owner of the keyboard, and publishes the answers back.
question_mutex: std.Io.Mutex = .init,
request: ?Request = null,
question_waiting: std.atomic.Value(bool) = .init(false),
question_served: std.atomic.Value(bool) = .init(false),
/// Set while the main thread is answering: the free-text row is read with
/// the same editor, so it must not hand the keyboard back to itself.
question_serving: std.atomic.Value(bool) = .init(false),
const Request = struct {
/// Borrowed from the worker, which keeps them alive until the form is
/// answered: it waits, and does nothing else, while it is on screen.
slots: []menu.Slot,
/// The answers, published back for the worker to take. Owned here until
/// it does.
replies: ?menu.Replies = null,
};
pub fn busy(self: *Runtime) bool {
return self.future != null;
}
pub fn deinit(self: *Runtime) void {
self.agent.interrupted.store(true, .release);
if (self.future) |*f| _ = f.cancel(self.agent.io);
_ = self.screen.drain() catch false;
self.screen.replay() catch {};
for (self.queue.items) |line| self.agent.allocator.free(line);
self.queue.deinit(self.agent.allocator);
self.dropQuestion();
}
fn dropQuestion(self: *Runtime) void {
self.question_mutex.lockUncancelable(self.agent.io);
defer self.question_mutex.unlock(self.agent.io);
if (self.request) |request| {
if (request.replies) |replies| replies.deinit(self.agent.allocator);
}
self.request = null;
self.question_waiting.store(false, .release);
}
pub fn interrupt(self: *Runtime) !void {
if (!self.busy()) return;
try self.stopRun(sgr.YELLOW ++ layout.indent ++ "Task interrupted (Esc). Input typed while it ran was discarded." ++ sgr.RESET ++ "\n");
}
/// End the run now: the worker is cancelled, nothing queued or typed during
/// it survives, and the screen says why. Both the prompt's Esc and the Esc
/// that cancels a question come through here.
fn stopRun(self: *Runtime, message: []const u8) !void {
self.agent.interrupted.store(true, .release);
if (self.future) |*f| f.cancel(self.agent.io);
self.future = null;
self.done.store(false, .release);
self.dropQuestion();
// Do not silently start another queued task after an explicit stop.
for (self.queue.items) |line| self.agent.allocator.free(line);
self.queue.clearRetainingCapacity();
// Neither should a line typed while the task ran outlive the stop.
self.agent.clearSteering();
try self.agent.finishInterruption();
_ = try self.screen.drain();
try self.screen.writeLog(message);
_ = try self.screen.drain();
}
pub fn submit(self: *Runtime, line: []const u8) !void {
// A running task takes the line itself: the worker sends it with its
// next request, so the instruction redirects the work in progress
// instead of waiting for it to end. A question is not answered here:
// while a question is open the menu has the keyboard, so nothing can
// reach this function from it.
if (self.busy()) {
try self.agent.steer(line);
try self.screen.writeLog(sgr.DIM ++ layout.indent ++ "Input sent to the running task." ++ sgr.RESET ++ "\n");
_ = try self.tick();
return;
}
const owned = try self.agent.allocator.dupe(u8, line);
self.queue.append(self.agent.allocator, owned) catch |err| {
self.agent.allocator.free(owned);
return err;
};
_ = try self.tick();
}
pub fn tick(self: *Runtime) !bool {
if (self.done.load(.acquire)) {
if (self.future) |*f| f.await(self.agent.io);
self.future = null;
self.done.store(false, .release);
// A line that arrived just as the task settled found no request
// left to join, so it runs as the next turn.
try self.agent.takeSteering(&self.queue);
}
if (!self.busy() and self.queue.items.len > 0) {
const line = self.queue.orderedRemove(0);
self.agent.interrupted.store(false, .release);
self.future = self.agent.io.concurrent(run, .{ self, line }) catch |err| {
self.agent.allocator.free(line);
return err;
};
}
return self.screen.drain();
}
fn run(self: *Runtime, line: []u8) void {
defer self.done.store(true, .release);
defer self.agent.allocator.free(line);
self.agent.processQuery(line) catch |err| {
if (self.agent.interrupted.load(.acquire)) return;
const message = std.fmt.allocPrint(self.agent.allocator, sgr.RED ++ layout.indent ++ "Error: {s}" ++ sgr.RESET ++ "\n", .{@errorName(err)}) catch return;
defer self.agent.allocator.free(message);
self.screen.writeLog(message) catch {};
};
self.screen.writeLog("\n") catch {};
}
/// The worker's side of a form: publish the questions and wait for the main
/// thread, which owns the keyboard, to put them to the user. Nothing is
/// drawn or read here — a worker that touched the terminal would fight the
/// form for the same keys.
pub fn presentForm(context: ?*anyopaque, slots: []menu.Slot) !menu.Replies {
const self: *Runtime = @ptrCast(@alignCast(context));
self.question_mutex.lockUncancelable(self.agent.io);
self.request = .{ .slots = slots };
self.question_served.store(false, .release);
self.question_mutex.unlock(self.agent.io);
self.question_waiting.store(true, .release);
// Short sleeps: the answers are published by the main thread, and an
// interrupt cancels this sleep, so the wait ends either way.
while (!self.question_served.load(.acquire)) {
try self.agent.io.sleep(.fromMilliseconds(20), .awake);
}
self.question_mutex.lockUncancelable(self.agent.io);
const replies = if (self.request) |request| request.replies else null;
self.request = null;
// Cleared under the mutex: the main thread must see the form is
// consumed before it looks for the next one, or it would draw the same
// questions twice.
self.question_waiting.store(false, .release);
self.question_mutex.unlock(self.agent.io);
// A dropped form is a dismissed one, so the worker always has an answer
// for every question, even when the run was interrupted under it.
return replies orelse menu.dismissedReplies(self.agent.allocator, slots.len);
}
/// Whether a question is waiting for the keyboard: the editor stops reading
/// the line and lets the menu take over.
pub fn questionPending(context: ?*anyopaque) bool {
const self: *Runtime = @ptrCast(@alignCast(context));
if (self.question_serving.load(.acquire)) return false;
return self.question_waiting.load(.acquire);
}
/// Put every pending question to the user, as one form, and hand the
/// answers back to the worker. The caller owns the keyboard: it has already
/// parked the line editor and the terminal's prompt rows are free.
pub fn serveQuestions(self: *Runtime) !void {
self.question_serving.store(true, .release);
defer self.question_serving.store(false, .release);
while (self.question_waiting.load(.acquire)) {
self.question_mutex.lockUncancelable(self.agent.io);
const request = self.request orelse {
self.question_mutex.unlock(self.agent.io);
return;
};
self.question_mutex.unlock(self.agent.io);
try self.serveForm(request.slots);
// Wait for the worker to take the answers before looking for the
// next form; otherwise this loop would draw the same one again.
while (self.question_waiting.load(.acquire)) {
try self.agent.io.sleep(.fromMilliseconds(2), .awake);
}
}
}
/// One form, from its questions to either an answer for every one of them
/// or a dismissal. Esc in the free-text row steps back to the form, so it
/// is drawn again until it is settled; Esc at the questions cancels them
/// and stops the run, as Esc at the prompt stops a task.
fn serveForm(self: *Runtime, slots: []menu.Slot) !void {
const replies = menu.resolve(self.agent.allocator, slots, .{
.context = self,
.present = presentOnScreen,
.read = readOnScreen,
}) catch |err| {
self.screen.endInput() catch {};
// The worker must not be left waiting for an answer that is never
// coming: it gets a dismissed form, and the run carries on.
self.settle(menu.dismissedReplies(self.agent.allocator, slots.len) catch return err) catch {};
return err;
};
if (replies.canceled) {
// Nothing is answered by a cancelled question, so the worker waits
// for no reply at all: the run ends here.
replies.deinit(self.agent.allocator);
try self.stopRun(sgr.YELLOW ++ layout.indent ++ "Task interrupted (Esc). The questions were cancelled and no answer was sent." ++ sgr.RESET ++ "\n");
return;
}
try self.settle(replies);
}
/// The screen owns geometry; the menu owns keyboard state and paints only
/// the dock above the status bar, never relative to the log's cursor.
fn presentOnScreen(context: *anyopaque, slots: []menu.Slot, focus: ?usize) anyerror!menu.Outcome {
const self: *Runtime = @ptrCast(@alignCast(context));
if (!self.screen.isActive()) return menu.form(self.agent.allocator, self.agent.io, slots, true, true, focus);
defer self.screen.endQuestion() catch {};
return menu.formDocked(self.agent.allocator, self.agent.io, slots, true, true, focus, .{
.context = self,
.prepare = prepareQuestion,
});
}
fn prepareQuestion(context: *anyopaque, wanted: usize) anyerror!menu.Viewport {
const self: *Runtime = @ptrCast(@alignCast(context));
try self.screen.reserveQuestion(wanted);
return .{ .top = self.screen.questionTop(), .height = self.screen.questionRows(), .columns = self.screen.cols };
}
/// Read the free text a free-text row asks for, with the same editor.
fn readOnScreen(context: *anyopaque) anyerror!menu.Line {
const self: *Runtime = @ptrCast(@alignCast(context));
return self.agent.readChoiceLine();
}
/// Publish the answers to the worker waiting in `presentForm`.
fn settle(self: *Runtime, replies: menu.Replies) !void {
self.question_mutex.lockUncancelable(self.agent.io);
defer self.question_mutex.unlock(self.agent.io);
if (self.request) |*request| {
if (request.replies) |old| old.deinit(self.agent.allocator);
request.replies = replies;
} else {
replies.deinit(self.agent.allocator);
}
self.question_served.store(true, .release);
}
};