Landin library reference discuss source

hosted/system/system.ldn

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