1-- The hosted provider's `io.programs`: a child is platform/posix's spawn,
2-- its pipes are descriptors this provider made, and waiting for it is
3-- POSIX's `waitpid`. Written once, over platform/posix, like the rest.
4
5import core/io
6import core/text
7import platform/posix
8
9-- One of a child's streams as the spawn makes it: the descriptor the
10-- child's stream is made from, the parent's end of its pipe when it is
11-- piped, and whether the child's side was made here, a pipe's end or
12-- /dev/null, and so is closed in the parent once the child has it.
13stream_end: type = struct
14 child_side: posix.descriptor
15 parent_side: posix.descriptor
16 made: bool
17 piped: bool
18end stream_end
19
20-- What a discarded stream is: the system's null device, which reads at
21-- once to its end and takes every write.
22null_device: []u8 = "/dev/null"
23
24-- Make the child's stream `target` as `choice` says; `output` when the
25-- child writes it.
26prepared: (choice: io.stream_choice, target: posix.descriptor, output: bool)
27 -> (result: stream_end) ! posix.errno =
28 result = (child_side: target, parent_side: target, made: false,
29 piped: false)
30 return when choice == io.inherited
31 if choice == io.discarded then
32 -- The device exists, and is neither created nor truncated.
33 options: posix.open_options =
34 (access: if output then posix.write_only else posix.read_only
35 end if,
36 create: false, exclusive: false, truncate: false,
37 append: false, directory: false)
38 loop do
39 made: posix.descriptor =
40 posix.open_at(posix.working_directory,
41 text.host_over(null_device), options,
42 0) else (problem)
43 continue when problem == posix.eintr
44 fail problem
45 end
46 result = (child_side: made, parent_side: made, made: true,
47 piped: false)
48 return
49 end loop
50 end if
51 ends: posix.pipe_ends = try posix.pipe()
52 result = if output then (child_side: ends.writing,
53 parent_side: ends.reading, made: true,
54 piped: true)
55 else (child_side: ends.reading, parent_side: ends.writing,
56 made: true, piped: true) end if
57end prepared
58
59-- Close what the spawn made for a stream: the child's side, which the
60-- child has its own copy of, and with `everything` the parent's end too,
61-- when no child was started.
62dropped: (given: stream_end, everything: bool) -> none =
63 if given.made then quietly_closed(given.child_side) end if
64 if everything and given.piped then
65 quietly_closed(given.parent_side)
66 end if
67end dropped
68
69-- Close a descriptor no caller is told about.
70quietly_closed: (closing: posix.descriptor) -> none =
71 posix.close(closing) else (problem)
72 _ = problem
73 end
74end quietly_closed
75
76-- posix.spawn reports a failure only when no child was started, so an
77-- interrupted spawn is made again.
78spawned: (path: text.host_string, arguments: []text.host_string,
79 environment: []io.variable, streams: posix.standard_streams)
80 -> (child: posix.process_id) ! posix.errno =
81 loop do
82 child = posix.spawn(path, arguments, environment,
83 streams) else (problem)
84 continue when problem == posix.eintr
85 fail problem
86 end
87 return
88 end loop
89end spawned
90
91launched: (path: text.host_string, arguments: []text.host_string,
92 environment: []io.variable, streams: io.child_streams)
93 -> (started: io.child) ! posix.errno =
94 input: stream_end = try prepared(streams.input, posix.standard_input,
95 false)
96 undo dropped(input, true)
97 output: stream_end = try prepared(streams.output, posix.standard_output,
98 true)
99 undo dropped(output, true)
100 errors: stream_end = try prepared(streams.errors, posix.standard_error,
101 true)
102 undo dropped(errors, true)
103 child: posix.process_id = try spawned(path, arguments, environment,
104 (input: input.child_side,
105 output: output.child_side,
106 errors: errors.child_side))
107 dropped(input, false)
108 dropped(output, false)
109 dropped(errors, false)
110 started = (handle: usize(i32(child)), input: file_of(input.parent_side),
111 output: file_of(output.parent_side),
112 errors: file_of(errors.parent_side),
113 input_open: input.piped, output_open: output.piped,
114 errors_open: errors.piped)
115end launched
116
117system_spawn: (self: system, path: text.host_string,
118 arguments: []text.host_string, environment: []io.variable,
119 streams: io.child_streams)
120 -> (started: io.child)
121 ! io.not_found | io.no_access | io.io_failed =
122 self.failure.val = 0
123 started = launched(path, arguments, environment, streams) else (problem)
124 record(self, problem)
125 fail io.not_found when absent(problem)
126 fail io.no_access when refused(problem)
127 fail io.io_failed
128 end
129end system_spawn
130
131-- Close one of a child's pipes, which is closed even when the system
132-- reports a failure.
133close_pipe: (self: system, opened: io.file) -> none ! io.io_failed =
134 posix.close(descriptor_of(opened)) else (problem)
135 record(self, problem)
136 fail io.io_failed
137 end
138end close_pipe
139
140-- Write all of BYTES to the child's input pipe, its descriptor, as a
141-- write to any file of this provider is made.
142system_write_child: (self: system, inout started: io.child, bytes: []u8)
143 -> none ! io.io_failed =
144 self.failure.val = 0
145 fail io.io_failed when not started.input_open
146 try system_write(self, started.input, bytes)
147end system_write_child
148
149system_close_input: (self: system, inout started: io.child)
150 -> none ! io.io_failed =
151 self.failure.val = 0
152 fail io.io_failed when not started.input_open
153 started.input_open = false
154 try close_pipe(self, started.input)
155end system_close_input
156
157-- Whether the child's errors, and not its output, is what to read next:
158-- the output when it has anything, its end included, and the errors only
159-- when they alone do. A wait a signal interrupted is made again.
160errors_first: (self: system, started: io.child) -> (yes: bool)
161 ! io.io_failed =
162 mut entries: [2]posix.poll_entry =
163 [posix.watch(descriptor_of(started.output), posix.poll_input),
164 posix.watch(descriptor_of(started.errors), posix.poll_input)]
165 loop do
166 _ = posix.poll(entries[0..<2], -1) else (problem)
167 continue when problem == posix.eintr
168 record(self, problem)
169 fail io.io_failed
170 end
171 break
172 end loop
173 yes = posix.happened(entries[0]) == posix.poll_no_events
174end errors_first
175
176system_read_child: (self: system, inout started: io.child, into: []mut u8)
177 -> (taken: io.child_read) ! io.io_failed =
178 self.failure.val = 0
179 taken = (count: 0, errors: false,
180 finished: not started.output_open and not started.errors_open)
181 return when lenof into == 0
182 loop do
183 if not started.output_open and not started.errors_open then
184 taken = (count: 0, errors: false, finished: true)
185 return
186 end if
187 mut from_errors: bool = not started.output_open
188 if started.output_open and started.errors_open then
189 from_errors = try errors_first(self, started)
190 end if
191 opened: io.file = if from_errors then started.errors
192 else started.output end if
193 count: usize = try system_read(self, opened, into)
194 if count > 0 then
195 taken = (count: count, errors: from_errors, finished: false)
196 return
197 end if
198 -- That stream has ended, and its pipe is done with.
199 if from_errors then
200 started.errors_open = false
201 else
202 started.output_open = false
203 end if
204 try close_pipe(self, opened)
205 end loop
206end system_read_child
207
208-- Close every pipe the child still has, keeping the first failure.
209close_pipes: (self: system, started: io.child) -> (closed: bool) =
210 closed = true
211 if started.input_open and not pipe_closed(self, started.input) then
212 closed = false
213 end if
214 if started.output_open and not pipe_closed(self, started.output) then
215 closed = false
216 end if
217 if started.errors_open and not pipe_closed(self, started.errors) then
218 closed = false
219 end if
220end close_pipes
221
222-- Whether a pipe closed without a failure.
223pipe_closed: (self: system, opened: io.file) -> (closed: bool) =
224 closed = true
225 close_pipe(self, opened) else (problem)
226 _ = problem
227 closed = false
228 end
229end pipe_closed
230
231-- Wait for the child, again when a signal interrupts the wait.
232waited: (self: system, child: posix.process_id)
233 -> (status: posix.child_status) ! io.io_failed =
234 loop do
235 status = posix.wait(child) else (problem)
236 continue when problem == posix.eintr
237 record(self, problem)
238 fail io.io_failed
239 end
240 return
241 end loop
242end waited
243
244-- The pipes are closed before the wait, so a child still writing to one
245-- meets a closed pipe rather than waiting for a reader that never comes.
246system_wait: (self: system, sink started: io.child)
247 -> (ended: io.ending) ! io.io_failed =
248 self.failure.val = 0
249 self.signal.val = 0
250 closed: bool = close_pipes(self, started)
251 status: posix.child_status =
252 try waited(self, posix.process_id(i32(started.handle)))
253 match status.how
254 posix.exited(code): ended = (how: io.exited(code: code))
255 posix.signalled(number): begin
256 self.signal.val = number
257 ended = (how: io.signalled)
258 end
259 end match
260 fail io.io_failed when not closed
261end system_wait
262
263system is io.programs
264 (spawn: system_spawn, write_child: system_write_child,
265 close_input: system_close_input, read_child: system_read_child,
266 wait: system_wait)