UnixSocket

UnixSocket :: # (opaque)

Exchange bytes and lines over native pathname AF_UNIX stream sockets. Supported on Linux, macOS, and Windows 10 1803+. No path is removed implicitly. The buffering, timeouts, and record limits match Tcp. Unix path bytes are preserved; the native filesystem decides which names it accepts. Windows paths must be representable as UTF-8. Native pathname length limits apply, and abstract or NUL-containing addresses are rejected.

listen! : Path => Try(Listener, [Interrupted, UnixSocketErr(IOErr)])

Bind a native pathname and listen. An existing pathname is never removed. After closing, explicitly call path.delete!() before binding it again.

connect! : Path, { connect_timeout : Duration, io_timeout : Duration } => Try(Connection, [Interrupted, UnixSocketErr(IOErr)])

Connect to a native pathname stream socket. Unix paths preserve raw bytes; Windows 10 1803+ uses UTF-8 pathname addresses. Native length limits apply.

Listener

UnixSocket.Listener :: # (opaque)

A listening socket.

The socket is closed when the last reference to the listener is released, or earlier by [close!]. Copies of a listener share one socket, so closing one closes them all.

to_inspect : Listener -> Str

Render the listener without exposing its host handle.

accept! : Listener, { accept_timeout : [NoTimeout, After(Duration)], io_timeout : Duration } => Try(Connection, [Closed, Interrupted, UnixSocketErr(IOErr)])

Wait for a client to connect.

accept_timeout bounds this call only: NoTimeout waits indefinitely, and After(duration) gives up with Err(UnixSocketErr(TimedOut)), after which the listener can accept again. io_timeout is the timeout the new Connection holds for its reads and writes; a connection never inherits the accept wait.

Closed after [close!].

match listener.accept!({ accept_timeout: After(Duration.seconds(1)), io_timeout: Duration.seconds(30) }) {
    Ok(conn) => serve!(conn)?
    Err(UnixSocketErr(TimedOut)) => {}
    Err(Closed) => { return Ok({}) }
    Err(UnixSocketErr(err)) => { return Err(UnixSocketErr(err)) }
}
close! : Listener => Try({}, [UnixSocketErr(IOErr)])

Stop listening. Closing again succeeds; the pathname remains. Connections already accepted stay open.

Connection

UnixSocket.Connection :: # (opaque)

A buffered stream connection, with the I/O timeout and line limit its reads and writes use.

The connection is closed when the last reference to it is released. Copies of a connection share one socket and one buffer, so every read, whether of lines, delimited bytes or binary data, continues where the previous one stopped. Connection.with_timeout and Connection.with_line_limit change the settings of the copy they return, not the socket.

Every read ends the same way: once the peer has closed its side and all data has been returned, the next read is Err(EndOfInput). An empty Ok is real data, such as an empty line.

A read that times out consumes nothing. Bytes that had arrived stay buffered for the next read, so a client may simply read again. A write that times out may have sent any prefix of its bytes: drop the connection unless the protocol can resynchronise.

to_inspect : Connection -> Str

Render the connection without exposing its host handle.

with_timeout : Connection, Duration -> Connection

The same connection with another I/O timeout. Each read or write through the result is bounded as a whole by timeout.

# Poll: is a line already here?
match conn.with_timeout(Duration.millis(10)).read_line!() {
    Ok(line) => handle!(line)?
    Err(UnixSocketErr(TimedOut)) => {}
    Err(err) => { return Err(err) }
}
with_line_limit : Connection, U64 -> Connection

The same connection with another limit, in bytes, on one record read by [read_until!], [read_line!] or Connection.lines. The delimiter or line terminator counts towards the limit. The default is 1,048,576 (1 MiB).

read_up_to! : Connection, U64 => Try(List(U8), [EndOfInput, Interrupted, UnixSocketErr(IOErr)])

Read at most max_bytes bytes, returning as soon as any have arrived. Short reads are valid. Asking for zero bytes returns Ok([]) without waiting; otherwise the result is never empty, and a closed peer is Err(EndOfInput).

read_exactly! : Connection, U64 => Try(List(U8), [UnexpectedEndOfInput, Interrupted, UnixSocketErr(IOErr)])

Read exactly count bytes. Zero succeeds without waiting.

If the peer closes before count bytes arrive the result is Err(UnexpectedEndOfInput). The bytes that did arrive are not consumed: [read_up_to!] returns them.

read_until! : Connection, U8 => Try(List(U8), [EndOfInput, Interrupted, ReadLimitExceeded(U64), UnixSocketErr(IOErr)])

Read bytes through and including the next delimiter.

If the peer closes first, the bytes before the end are returned without a delimiter, and the read after that is Err(EndOfInput). So the result is never empty, and its last byte shows whether the delimiter arrived.

A record longer than the connection's line limit is Err(ReadLimitExceeded(limit)). The rest of that record, through its delimiter, is then discarded before the next read returns, as part of that read and within its timeout.

field = conn.read_until!('|')?
read_line! : Connection => Try(Str, [EndOfInput, ReadLimitExceeded(U64), BadUtf8({ problem : Utf8Problem, index : U64 }), Interrupted, UnixSocketErr(IOErr)])

Read a line of text.

A line ends at \n. The terminator is not returned, and one \r before it is removed. If the peer closes without a final \n the last line is still returned; after that the result is Err(EndOfInput). An empty line is Ok("").

A line longer than the connection's line limit is Err(ReadLimitExceeded(limit)), and a line that is not valid UTF-8 is BadUtf8. Either way that line is skipped and the next call reads the line after it. For the bytes of a line, use conn.read_until!('\n').

write_bytes! : Connection, List(U8) => Try({}, [Interrupted, UnixSocketErr(IOErr)])

Write all of bytes. They are sent at once, not buffered.

write_utf8! : Connection, Str => Try({}, [Interrupted, UnixSocketErr(IOErr)])

Write a string as UTF-8. It is sent at once, not buffered.

chunks : Connection -> Stream(Try(List(U8), [Interrupted, UnixSocketErr(IOErr)]))

Lazily read this connection as non-empty chunks of at most 64 KiB, each returned as soon as it arrives. Chunk boundaries do not correspond to the peer's writes, records, or text encoding boundaries. For a chosen size, call [read_up_to!] in a loop.

Building the stream reads nothing; each item is one read with the connection's timeout. The stream ends when the peer closes. A UnixSocketErr item, including UnixSocketErr(TimedOut), is the last one; the connection stays usable and a new stream may be started.

for! item in conn.chunks() {
    bytes = item?
    $total = $total + bytes.len()
}

The stream holds no data of its own, so it can be mixed with the other reads and with writes. It keeps the connection open until it has finished or is released.

lines : Connection -> Stream(Try(Str, [ReadLimitExceeded(U64), BadUtf8({ problem : Utf8Problem, index : U64 }), Interrupted, UnixSocketErr(IOErr)]))

Lazily read this connection a line at a time, as [read_line!] does.

Building the stream reads nothing; each item is one read with the connection's timeout and line limit. The stream ends when the peer closes. BadUtf8 and ReadLimitExceeded items concern one line, and the stream continues with the next. A UnixSocketErr item, including UnixSocketErr(TimedOut), is the last one; the connection stays usable, so a client that tolerates idle periods calls conn.lines() again.

for! item in conn.lines() {
    match item {
        Ok(line) => Stdout.line!(line)?
        Err(BadUtf8(_)) => {}
        Err(ReadLimitExceeded(_)) => {}
        Err(UnixSocketErr(err)) => { return Err(UnixSocketErr(err)) }
    }
}

The stream holds no data of its own, so it can be mixed with the other reads and with writes, and leaving the loop early loses nothing. It keeps the connection open until it has finished or is released.