hosted/system/programs.ldn
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 none
72end quietly_closed
73
74-- posix.spawn reports a failure only when no child was started, so an
75-- interrupted spawn is made again.
76spawned: (path: text.host_string, arguments: []text.host_string,
77 environment: []io.variable, streams: posix.standard_streams)
78 -> (child: posix.process_id) ! posix.errno =
79 loop do
80 child = posix.spawn(path, arguments, environment,
81 streams) else (problem)
82 continue when problem == posix.eintr
83 fail problem
84 end
85 return
86 end loop
87end spawned
88
89launched: (path: text.host_string, arguments: []text.host_string,
90 environment: []io.variable, streams: io.child_streams)
91 -> (started: io.child) ! posix.errno =
92 input: stream_end = try prepared(streams.input, posix.standard_input,
93 false)
94 undo dropped(input, true)
95 output: stream_end = try prepared(streams.output, posix.standard_output,
96 true)
97 undo dropped(output, true)
98 errors: stream_end = try prepared(streams.errors, posix.standard_error,
99 true)
100 undo dropped(errors, true)
101 child: posix.process_id = try spawned(path, arguments, environment,
102 (input: input.child_side,
103 output: output.child_side,
104 errors: errors.child_side))
105 dropped(input, false)
106 dropped(output, false)
107 dropped(errors, false)
108 started = (handle: usize(i32(child)), input: file_of(input.parent_side),
109 output: file_of(output.parent_side),
110 errors: file_of(errors.parent_side),
111 input_open: input.piped, output_open: output.piped,
112 errors_open: errors.piped)
113end launched
114
115system_spawn: (self: host, path: text.host_string,
116 arguments: []text.host_string, environment: []io.variable,
117 streams: io.child_streams)
118 -> (started: io.child)
119 ! io.not_found | io.no_access | io.io_failed =
120 self.failure.val = 0
121 started = launched(path, arguments, environment, streams) else (problem)
122 try path_failure(self, problem)
123 fail io.io_failed
124 end
125end system_spawn
126
127-- Close one of a child's pipes, which is closed even when the system
128-- reports a failure.
129close_pipe: (self: host, opened: io.file) -> none ! io.io_failed =
130 posix.close(descriptor_of(opened)) else (problem)
131 record(self, problem)
132 fail io.io_failed
133 end
134end close_pipe
135
136-- Write all of BYTES to the child's input pipe, its descriptor, as a
137-- write to any file of this provider is made.
138system_write_child: (self: host, inout started: io.child, bytes: []u8)
139 -> none ! io.io_failed =
140 self.failure.val = 0
141 fail io.io_failed when not started.input_open
142 try system_write(self, started.input, bytes)
143end system_write_child
144
145system_close_input: (self: host, inout started: io.child)
146 -> none ! io.io_failed =
147 self.failure.val = 0
148 fail io.io_failed when not started.input_open
149 started.input_open = false
150 try close_pipe(self, started.input)
151end system_close_input
152
153-- Whether the child's errors, and not its output, is what to read next:
154-- the output when it has anything, its end included, and the errors only
155-- when they alone do. A wait a signal interrupted is made again.
156errors_first: (self: host, started: io.child) -> (yes: bool)
157 ! io.io_failed =
158 mut entries: [2]posix.poll_entry =
159 [posix.watch(descriptor_of(started.output), posix.poll_input),
160 posix.watch(descriptor_of(started.errors), posix.poll_input)]
161 loop do
162 _ = posix.poll(entries[0..<2], -1) else (problem)
163 continue when problem == posix.eintr
164 record(self, problem)
165 fail io.io_failed
166 end
167 break
168 end loop
169 yes = posix.happened(entries[0]) == posix.poll_no_events
170end errors_first
171
172system_read_child: (self: host, inout started: io.child, into: []mut u8)
173 -> (taken: io.child_read) ! io.io_failed =
174 self.failure.val = 0
175 taken = (count: 0, stream: io.child_output,
176 finished: not started.output_open and not started.errors_open)
177 return when lenof into == 0
178 loop do
179 if not started.output_open and not started.errors_open then
180 taken = (count: 0, stream: io.child_output, finished: true)
181 return
182 end if
183 mut from_errors: bool = not started.output_open
184 if started.output_open and started.errors_open then
185 from_errors = try errors_first(self, started)
186 end if
187 opened: io.file = if from_errors then started.errors
188 else started.output end if
189 count: usize = try system_read(self, opened, into)
190 if count > 0 then
191 taken = (count: count,
192 stream: if from_errors then io.child_errors
193 else io.child_output end if,
194 finished: false)
195 return
196 end if
197 -- That stream has ended, and its pipe is done with.
198 if from_errors then
199 started.errors_open = false
200 else
201 started.output_open = false
202 end if
203 try close_pipe(self, opened)
204 end loop
205end system_read_child
206
207-- Close every pipe the child still has, keeping the first failure.
208close_pipes: (self: host, started: io.child) -> (closed: bool) =
209 closed = true
210 if started.input_open then
211 close_pipe(self, started.input) else (problem)
212 _ = problem
213 closed = false
214 end
215 end if
216 if started.output_open then
217 close_pipe(self, started.output) else (problem)
218 _ = problem
219 closed = false
220 end
221 end if
222 if started.errors_open then
223 close_pipe(self, started.errors) else (problem)
224 _ = problem
225 closed = false
226 end
227 end if
228end close_pipes
229
230-- Wait for the child, again when a signal interrupts the wait.
231waited: (self: host, child: posix.process_id)
232 -> (status: posix.child_status) ! io.io_failed =
233 loop do
234 status = posix.wait(child) else (problem)
235 continue when problem == posix.eintr
236 record(self, problem)
237 fail io.io_failed
238 end
239 return
240 end loop
241end waited
242
243-- The pipes are closed before the wait, so a child still writing to one
244-- meets a closed pipe rather than waiting for a reader that never comes.
245system_wait: (self: host, sink started: io.child)
246 -> (ended: io.ending) ! io.io_failed =
247 self.failure.val = 0
248 self.signal.val = 0
249 closed: bool = close_pipes(self, started)
250 status: posix.child_status =
251 try waited(self, posix.process_id(i32(started.handle)))
252 match status.how
253 posix.exited(code): ended = (how: io.exited(code: code))
254 posix.signalled(number): begin
255 self.signal.val = number
256 ended = (how: io.signalled)
257 end
258 end match
259 fail io.io_failed when not closed
260end system_wait
261
262host is io.programs
263 (spawn: system_spawn, write_child: system_write_child,
264 close_input: system_close_input, read_child: system_read_child,
265 wait: system_wait)