Landin library reference discuss source

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)