1-- The provider of every core/io concept on hosted targets, `host`: reader,
2-- writer, files and process, and so of their composition, world, and
3-- directories, the environment, programs, entropy, the rest of the file
4-- system and terminals beside them, and core/time's clocks and core/net's
5-- network too, each in a file named as core/io names it. It is
6-- written once, over platform/posix, which reaches Linux through its
7-- system calls and Darwin and FreeBSD through their C library: nothing
8-- here is written per system. Import this module only at a hosted
9-- authority root.
10
11import core/cmp
12import core/io
13import core/text
14import platform/posix
15
16-- A file open_write creates gets 0666, which the process umask filters.
17created_mode: u32 = 0o666
18
19-- A directory made by make_directory gets 0777, which the umask filters.
20created_directory_mode: u32 = 0o777
21
22-- The process's last-error cell, which every handle from `host` shares,
23-- and its last-signal cell beside it.
24mut process_errno: i32
25mut process_signal: i32
26
27system_value: type = struct
28 failure: ptr mut i32
29 signal: ptr mut i32
30end system_value
31
32--- Hosted implementation of all four I/O capabilities, `io.world`,
33--- `io.directories`, `io.environment`, `io.programs`, the clocks
34--- `time.clock` and `time.wall_clock`, the system's `io.entropy`,
35--- `net.network`, `io.file_system` and `io.terminals`, over platform/posix,
36--- as a handle:
37--- copies share the process's descriptors, arguments and environment, and
38--- the last-error and last-signal cells they were made with. File
39--- identifiers belong to this provider; borrowed arguments and variables
40--- refer to process storage.
41public host: type = system_value
42
43--- A last-error cell and a last-signal cell of the caller's own, for code
44--- that must not share the process's: a C thread calling into Landin, or
45--- a library keeping its own detail. Declare it `zeroed` and make its
46--- handle with `over`.
47public space: type = struct
48 failure: i32
49 signal: i32
50end space
51
52--- Return errno captured for the terminal failing provider call. Read it
53--- immediately after failure; it belongs to the cell the handle was made
54--- with, the process's from `host` or the caller's from `over`, and is not a
55--- portable error category.
56---
57--- Zero means that the handle's most recent call of any concept did not
58--- fail with an errno: each call clears the cell before it asks the system.
59--- A nonzero value is the exact errno captured for the terminal call before
60--- the provider made any other host call: its number
61--- as the system gives it, platform/posix's `number` of the atom the call
62--- failed with. Retriable EINTR attempts are internal and do not replace
63--- a previously reported operation with a failure.
64public last_errno: (handle: host) -> (number: i32) =
65 number = handle.failure.val
66end last_errno
67
68--- Return the number of the signal that ended the child this handle's
69--- cell last saw end through `io.wait`, as POSIX numbers it on this
70--- system: SIGTERM is 15 and SIGKILL 9 on Linux, Darwin and FreeBSD alike.
71--- Zero means that child exited with a code, or that no child has been
72--- waited for. It is the system's detail behind `io.ending`'s portable
73--- `signalled`, as `last_errno` is behind an error atom.
74public last_signal: (handle: host) -> (number: i32) =
75 number = handle.signal.val
76end last_signal
77
78-- Keep a failure's errno in the handle's cell; `unlisted`, which no errno
79-- describes, leaves the cell at zero.
80record: (self: host, problem: posix.errno) -> none =
81 number: i32 = posix.number(problem)
82 if number > 0 then self.failure.val = number end if
83end record
84
85-- A path names nothing when a component is absent, ENOENT, or when a
86-- component before the last is not a directory, ENOTDIR: in both cases
87-- there is no file at that path for an open to find.
88absent: (problem: posix.errno) -> (yes: bool) =
89 yes = problem == posix.enoent or problem == posix.enotdir
90end absent
91
92-- Whether an errno says the caller may not do what it asked.
93refused: (problem: posix.errno) -> (yes: bool) =
94 yes = problem == posix.eperm or problem == posix.eacces
95 or problem == posix.erofs
96end refused
97
98-- Keep a failure about a path in the handle's cell and fail as the
99-- concepts say of one: `not_found` for no file there, `no_access` when it
100-- is refused, and `io_failed` for the rest. The one mapping every call
101-- on a path uses.
102path_failure: (self: host, problem: posix.errno)
103 -> none ! io.not_found | io.no_access | io.io_failed =
104 record(self, problem)
105 fail io.not_found when absent(problem)
106 fail io.no_access when refused(problem)
107 fail io.io_failed
108end path_failure
109
110-- A socket this provider made carries this bit beside its descriptor,
111-- which is never negative and so never has it, so that `io.write` sends
112-- on it rather than writing: Linux has no option that keeps a socket's
113-- `write` from raising SIGPIPE, and only `send` takes MSG_NOSIGNAL (D322).
114socket_mark: usize = 0x80000000
115
116--- The platform/posix descriptor a file of this provider is, for a call
117--- the portable concepts do not make: a file's permission bits, a lock, a
118--- terminal's raw mode. The file stays this provider's, to close through
119--- `io.close`.
120public descriptor_of: (opened: io.file) -> (result: posix.descriptor) =
121 result = posix.descriptor(i32(usize(opened) & (socket_mark - 1)))
122end descriptor_of
123
124--- The file of this provider a platform/posix descriptor is, so `io.read`,
125--- `io.write` and the rest take one that platform/posix opened. Close it
126--- once, through either. A socket platform/posix made is written with
127--- POSIX's `write` through it, which on Linux raises SIGPIPE when its peer
128--- has gone; `socket_of` makes one whose writes are sends.
129public file_of: (opened: posix.descriptor) -> (result: io.file) =
130 result = io.file(usize(i32(opened)))
131end file_of
132
133--- The file of this provider a platform/posix socket is: `io.write` sends
134--- on it as `net.send` does, so a peer that has gone fails the write with
135--- EPIPE on every system and raises no SIGPIPE, beside a C handler too.
136--- `net.connect`, `net.listen`, `net.accept` and `net.open_datagram` give
137--- their sockets so.
138public socket_of: (opened: posix.descriptor) -> (result: io.file) =
139 result = io.file(usize(i32(opened)) | socket_mark)
140end socket_of
141
142-- Write some of `bytes` to `opened`: a send on a socket, a write
143-- otherwise.
144put_some: (opened: io.file, bytes: []u8) -> (count: usize) ! posix.errno =
145 if (usize(opened) & socket_mark) <> 0 then
146 count = try posix.send(descriptor_of(opened), bytes)
147 else
148 count = try posix.write(descriptor_of(opened), bytes)
149 end if
150end put_some
151
152-- Open `path` with `options`, a file it creates getting `mode` before the
153-- umask: the one open every concept's makes. open
154-- reports EINTR before publishing a descriptor, so the same request is
155-- retried; every other errno is kept before it is mapped to the stable
156-- atom set, a name an exclusive open finds taken `already_exists`.
157open: (self: host, path: text.host_string, options: posix.open_options,
158 mode: u32) -> (opened: io.file)
159 ! io.already_exists | io.not_found | io.no_access | io.io_failed =
160 self.failure.val = 0
161 loop do
162 made: posix.descriptor =
163 posix.open_at(posix.working_directory, path, options, mode)
164 else (problem)
165 continue when problem == posix.eintr
166 if problem == posix.eexist then
167 record(self, problem)
168 fail io.already_exists
169 end if
170 try path_failure(self, problem)
171 fail io.io_failed
172 end
173 opened = file_of(made)
174 return
175 end loop
176end open
177
178system_open_read: (self: host, path: text.host_string)
179 -> (opened: io.file)
180 ! io.not_found | io.no_access | io.io_failed =
181 opened = open(self, path, posix.reading, created_mode) else (problem)
182 fail io.not_found when problem == io.not_found
183 fail io.no_access when problem == io.no_access
184 fail io.io_failed
185 end
186end system_open_read
187
188system_open_write: (self: host, path: text.host_string)
189 -> (opened: io.file)
190 ! io.not_found | io.no_access | io.io_failed =
191 opened = open(self, path, posix.writing, created_mode) else (problem)
192 fail io.not_found when problem == io.not_found
193 fail io.no_access when problem == io.no_access
194 fail io.io_failed
195 end
196end system_open_write
197
198-- A stable-path identity check, not an atomic check-and-open operation.
199-- Every input lookup failure refuses; only an absent output is allowed.
200system_same_file: (self: host, left: text.host_string,
201 right: text.host_string)
202 -> (same: bool) ! io.io_failed =
203 self.failure.val = 0
204 first: posix.status =
205 posix.status_at(posix.working_directory, left, true) else (problem)
206 record(self, problem)
207 fail io.io_failed
208 end
209 same = false
210 second: posix.status =
211 posix.status_at(posix.working_directory, right, true) else (problem)
212 return when absent(problem)
213 record(self, problem)
214 fail io.io_failed
215 end
216 same = first.device == second.device and first.inode == second.inode
217end system_same_file
218
219system_close: (self: host, sink opened: io.file)
220 -> none ! io.io_failed =
221 self.failure.val = 0
222 -- A failed close may already have released the descriptor; retrying
223 -- could close an unrelated reused descriptor, so EINTR is
224 -- deliberately terminal here.
225 posix.close(descriptor_of(opened)) else (problem)
226 record(self, problem)
227 fail io.io_failed
228 end
229end system_close
230
231system_read: (self: host, opened: io.file, into: []mut u8)
232 -> (count: usize) ! io.io_failed =
233 self.failure.val = 0
234 if lenof into == 0 then
235 count = 0
236 return
237 end if
238 loop do
239 -- read returns EINTR only before transferring a byte, so the same
240 -- buffer and extent may be retried. A positive partial read is
241 -- returned and is never discarded by an unnecessary retry.
242 got: usize = posix.read(descriptor_of(opened), into) else (problem)
243 continue when problem == posix.eintr
244 record(self, problem)
245 fail io.io_failed
246 end
247 -- A count beyond the buffer violates the system's contract and
248 -- fabricates no errno. Zero from a nonempty read is genuine EOF.
249 fail io.io_failed when got > lenof into
250 count = got
251 return
252 end loop
253end system_read
254
255write_all: (self: host, opened: io.file, bytes: []u8)
256 -> none ! io.io_failed =
257 mut written: usize = 0
258 while written < lenof bytes do
259 remaining: []u8 = bytes[written..<lenof bytes]
260 -- write reports EINTR only before this attempt transfers data.
261 -- Retry the same remaining suffix; a preceding positive partial
262 -- write has already advanced `written` and is never repeated.
263 accepted: usize =
264 put_some(opened, remaining) else (problem)
265 continue when problem == posix.eintr
266 record(self, problem)
267 fail io.io_failed
268 end
269 -- A nonempty zero-progress success cannot complete write-all, and
270 -- an oversized count violates the system's contract. Neither
271 -- fabricates errno.
272 fail io.io_failed when accepted == 0 or accepted > lenof remaining
273 written += accepted
274 end while
275end write_all
276
277system_write: (self: host, opened: io.file, bytes: []u8)
278 -> none ! io.io_failed =
279 self.failure.val = 0
280 return when lenof bytes == 0
281 try write_all(self, opened, bytes)
282end system_write
283
284system_write_some: (self: host, opened: io.file, bytes: []u8)
285 -> (count: usize) ! io.io_failed =
286 self.failure.val = 0
287 if lenof bytes == 0 then
288 count = 0
289 return
290 end if
291 -- Keep a single host request within a representable, bounded extent;
292 -- the caller can send the remaining suffix after this progress.
293 mut requested: usize = lenof bytes
294 if requested > 65536 then requested = 65536 end if
295 loop do
296 -- EINTR transfers nothing on this attempt. A positive short write
297 -- returns immediately so its exact prefix reaches the caller.
298 accepted: usize = put_some(opened, bytes[0..<requested])
299 else (problem)
300 continue when problem == posix.eintr
301 record(self, problem)
302 fail io.io_failed
303 end
304 fail io.io_failed when accepted == 0 or accepted > requested
305 count = accepted
306 return
307 end loop
308end system_write_some
309
310system_input: (self: host) -> (stream: io.file) =
311 _ = self.failure
312 stream = file_of(posix.standard_input)
313end system_input
314
315system_out: (self: host) -> (stream: io.file) =
316 _ = self.failure
317 stream = file_of(posix.standard_output)
318end system_out
319
320system_err: (self: host) -> (stream: io.file) =
321 _ = self.failure
322 stream = file_of(posix.standard_error)
323end system_err
324
325system_argument_count: (self: host) -> (count: usize) =
326 _ = self.failure
327 table: posix.argument_table = posix.arguments()
328 count = table.count
329end system_argument_count
330
331-- The argument is lent from the process's argument root, storage that
332-- outlives every handle, which satisfies the concept's `from self`.
333system_argument: (self: host, index: usize)
334 -> (value: text.host_string from self) ! io.no_argument =
335 _ = self.failure
336 table: posix.argument_table = posix.arguments()
337 fail io.no_argument when index >= table.count
338 value = posix.argument(table, index)
339end system_argument
340
341-- The environment is platform/posix's, read where the process's entry
342-- left it, and lent from storage that outlives every handle, as an
343-- argument is.
344system_variable_count: (self: host) -> (count: usize) =
345 _ = self.failure
346 table: posix.environment_table = posix.environment()
347 count = table.count
348end system_variable_count
349
350system_variable_at: (self: host, index: usize)
351 -> (item: io.variable from self) ! io.no_variable =
352 _ = self.failure
353 table: posix.environment_table = posix.environment()
354 fail io.no_variable when index >= table.count
355 item = posix.variable(table, index)
356end system_variable_at
357
358system_variable_named: (self: host, name: text.host_string)
359 -> (value: text.host_string from self)
360 ! io.no_variable =
361 _ = self.failure
362 table: posix.environment_table = posix.environment()
363 wanted: []u8 = text.host_bytes(name)
364 for index in usize(0)..<table.count do
365 item: io.variable = posix.variable(table, index)
366 found: []u8 = text.host_bytes(item.name)
367 if cmp.compare(found, wanted) == cmp.equal then
368 value = item.value
369 return
370 end if
371 end for
372 fail io.no_variable
373end system_variable_named
374
375host is io.reader (read: system_read)
376host is io.writer (write: system_write, write_some: system_write_some)
377host is io.files
378 (open_read: system_open_read, open_write: system_open_write,
379 same_file: system_same_file, close: system_close)
380host is io.process
381 (input: system_input, out: system_out, err: system_err,
382 argument_count: system_argument_count, argument: system_argument)
383host is io.environment
384 (variable_count: system_variable_count,
385 variable_at: system_variable_at,
386 variable_named: system_variable_named)
387host is io.world ()
388
389--- Return a handle to the hosted I/O provider, exposing process arguments and
390--- the three standard streams, descriptors 0, 1 and 2. Regular open-write
391--- operations create or truncate files; close regular files explicitly. The
392--- handle borrows nothing: the argument root, the descriptors and its
393--- last-error cell belong to the process. Use `over` for a cell of the
394--- caller's own.
395---
396--- The arguments are the root platform/posix's
397--- `_landin_host_initialize_arguments` established, which the hosted entry
398--- establishes before its body runs; calling this before any root exists
399--- traps.
400public new: () -> (handle: host) =
401 _ = posix.arguments()
402 handle = (failure: addr process_errno, signal: addr process_signal)
403end new
404
405--- Fill `state` and return a handle that records its last error there
406--- rather than in the process's cell. It shares the process's arguments
407--- and descriptors like any other handle; calling this before any root
408--- exists traps, as `host` does.
409public over: (inout state: space) -> (handle: host from state) =
410 _ = posix.arguments()
411 state = (failure: 0, signal: 0)
412 handle = (failure: addr state.failure, signal: addr state.signal)
413end over