std/os

Environment, System, Memory, Processor, Volume, CurrentProcess, Directories, User, Entropy, MacAddress and NetworkInterface - the machine the program runs on - and OsError.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

Die generierte Referenz von std/os zeigt jede öffentliche Deklaration mit Signatur, Doc-Kommentar und Beispielen.

Draft. Slices 1 to 4 of docs/design/OS.md and the user folders and the user of slice 6: the environment, the system's identity, the base and user directories, the memory and swap, the processors, the current process, the volumes and the user. The C of the macOS and FreeBSD natives is compiled on those systems only and has not run there yet; network interfaces are built on Linux only and their traffic is a later slice.

std/os answers what a program asks about the machine it runs on: the environment it was started with, which operating system and version it is, the host name and how long the machine has been running, how much memory and swap it has and how much of it is available, which process it is itself, and where the user's configuration, data, cache and temporary files belong, which account it runs as - and it hands out the system's source of randomness. It re-exports OperatingSystem, Architecture and ByteOrder of std/core, so a file that branches on the system writes one import. It is deliberately not in the prelude: use System from "std/os" at the top of a file is the statement "this file asks the operating system".

Every question is a match OperatingSystem.current in TorbScript whose arm hands it to the directory of one system. Only the arm of the build's target is compiled, and every arm is type checked on every machine (see Compile-time branches).

Import#

use Environment, EnvironmentVariables, System, SystemVersion, Memory, Swap, Directories, OsError from "std/os"
use Processor, ProcessorTimes, ProcessorUsage, ProcessorDescription, ProcessorCache, ProcessorFeature, Frequency from "std/os"
use CurrentProcess, User from "std/os"
use Volume, VolumeKind, VolumeSpace from "std/os"
use Entropy from "std/os/entropy"
use MacAddress from "std/os"
use NetworkInterface, InterfaceAddress, InterfaceStatus, InterfaceKind from "std/os"
use System, Directories, OsError from "std/os"
use Path from "std/path"

fn main(): Result<Void, OsError> {
  const version = System.version()?
  print "{version.name} ({version.kernel}) on {System.hostName()?}"
  const settings = Directories.configuration()?
  print settings.joined(Path.from("my-tool"))
  Ok void
}

Declarations#

Environment#

native type Environment {
  native static fn get(name: String): String?
  static fn variables(): EnvironmentVariables
  static fn searchPath(): List<Path>
}

The environment this process was started with. get(name) answers None if the variable is not set, or if the caller is not allowed to see it - the sandbox and a variable that does not exist look the same on purpose. On Windows a name is looked up without regard to case, as the system does. variables() is every variable the caller may see, as a value; searchPath() is PATH split at the system's separator (; on Windows, : elsewhere), each entry a Path, the empty ones dropped.

There is no Environment.set: changing a variable of the running process is global mutable state, and on POSIX a data race besides. What setting a variable is for is a child process, and a child is handed an EnvironmentVariables value.

Inside a sandboxed script the module std/os/environment has to be granted, and only the variables a granted pattern matches (environment "APP_*") are visible to get and variables.

EnvironmentVariables#

type EnvironmentVariables with Show, Equals {
  static fn empty(): Self
  fn get(name: String): String?
  fn names(): List<String>
  var fn set(name: String, value: String)
  var fn remove(name: String)
  fn show(): String
}

A set of environment variables as a value. Names compare the way the target compares them: without regard to case on Windows, exactly everywhere else, so set("PATH", …) on Windows replaces an inherited Path. show() lists the names only, because an environment carries secrets and a print of it must not.

use EnvironmentVariables from "std/os"

var variables = EnvironmentVariables.empty()
variables.set "APP_MODE", "debug"
variables.set "NO_COLOR", "1"
variables.remove "NO_COLOR"
print variables

System#

type System {
  static fn version(): Result<SystemVersion, OsError>
  static fn hostName(): Result<String, OsError>
  static fn uptime(): Result<Duration, OsError>
  static fn pageSize(): ByteSize
  static fn machineArchitecture(): Result<Architecture, OsError>
}
WindowsLinuxmacOSFreeBSD
versionregistry ProductName, DisplayVersion, UBR; RtlGetVersion/etc/os-release, unamekern.osproductversion, kern.osversion, kern.osreleaseuname
hostNameGetComputerNameExWgethostnamegethostnamegethostname
uptimeGetTickCount64/proc/uptimekern.boottimekern.boottime
pageSizeGetNativeSystemInfosysconfsysconfsysconf
machineArchitectureIsWow64Process2unameuname, sysctl.proc_translateduname

hostName never asks DNS. uptime includes the time the machine was asleep on every system. pageSize is a ByteSize and never fails. In the browser, where the playground runs a program, version, hostName and uptime are Unsupported, pageSize is WebAssembly's 65536, and machineArchitecture is .Wasm64. machineArchitecture is the machine and not always Architecture.current: an x86-64 program emulated on Arm64 Windows, or translated by Rosetta on a Mac, runs on .Arm64.

SystemVersion#

type SystemVersion with Show, Equals {
  name: String
  identifier: String
  release: String
  build: String?
  kernel: String
  fn kernelNumbers(): List<Int>
}

name is for people (Windows 11 Pro, Ubuntu 24.04.1 LTS, macOS 15.1, FreeBSD 14.1-RELEASE); identifier is for programs, stable and lower case (windows, ubuntu, macos, freebsd); release is the product's own version (24H2, 24.04, 15.1, 14.1); build is there where the product has one (26100.2314, 24B83); kernel is the kernel's release, and kernelNumbers() its leading numbers for comparing ([6, 8, 0] of 6.8.0-45-generic).

Windows 11 still writes Windows 10 into the registry's ProductName, and the name says Windows 11 from build 22000 on.

Memory#

type Memory with Show, Equals {
  total: ByteSize
  available: ByteSize
  swap: Swap
  fn used(): ByteSize
  static fn current(): Result<Memory, OsError>
}
type Swap with Show, Equals {
  total: ByteSize
  used: ByteSize
  fn free(): ByteSize
}

Memory.current() is how much memory the machine has (total), how much of it a new allocation could still get without swapping (available), and its swap; every size is a ByteSize of std/number.

use Memory, OsError from "std/os"

fn main(): Result<Void, OsError> {
  const memory = Memory.current()?
  print "{memory.available} of {memory.total} available, {memory.used()} used"
  print "swap: {memory.swap.used} of {memory.swap.total}"
  Ok void
}
totalavailableswap
WindowsGlobalMemoryStatusEx, ullTotalPhysullAvailPhysthe page files, from NtQuerySystemInformation(SystemPageFileInformation)
LinuxMemTotal of /proc/meminfoMemAvailable, or MemFree + Buffers + Cached before 3.14SwapTotal, SwapTotal - SwapFree
macOShw.memsizethe free (speculative included), inactive and purgeable pages of host_statistics64vm.swapusage

available is each system's own estimate, and it counts the caches the system would give up: Linux and macOS use free memory as a cache on purpose, so the memory nothing has claimed is small on a busy machine and says little. That is why used() is total - available, never a number of the system's own, and why it is the one to watch. The swap of Windows is the page files alone, never the commit limit, which is the memory and the page files together. FreeBSD answers Unsupported until its slice, and so does the browser, which cannot tell. A /proc/meminfo that is not in the form Linux writes it is OsError.Malformed.

Processor#

type Processor {
  static fn logicalCount(): Result<Int, OsError>
  static fn physicalCount(): Result<Int, OsError>
  static fn describe(): Result<ProcessorDescription, OsError>
  static fn times(): Result<ProcessorTimes, OsError>
  static fn timesOfEach(): Result<List<ProcessorTimes>, OsError>
  static fn usage(interval: Duration = 1.seconds()): Task<Result<ProcessorUsage, OsError>>
}
type ProcessorTimes with Show, Equals, Subtract {
  user: Duration
  system: Duration
  idle: Duration
  waiting: Duration
  other: Duration
  fn total(): Duration
  fn subtract(other: Self): Self
}
type ProcessorUsage with Show, Equals {
  busy: Float64
  user: Float64
  system: Float64
  each: List<Float64>
  static fn between(total: ProcessorTimes, each: List<ProcessorTimes>): ProcessorUsage
}
type Frequency with Show, Compare {
  static fn ofHertz(hertz: Int64): Frequency
  static fn ofMegahertz(megahertz: Int64): Frequency
  fn hertz(): Int64
  fn megahertz(): Float64
  fn gigahertz(): Float64
  fn compare(other: Self): Ordering
  fn show(): String
}

Processor.logicalCount() is how many threads the system runs at once, hyper-threads included, and physicalCount() how many cores they sit on; the first is never below one and the second never above the first. times() is the time all processors together have spent in each state since the system started, and timesOfEach() the same for every logical processor, in the system's order. Both are Durations that only grow, and the difference of two readings (after - before) is the time that passed in between. usage(interval) takes the two readings with a sleep between them and answers the fractions: busy is user + system of the total time, each the same for every processor. A difference that covers no time answers 0, never NaN. A Frequency is a clock in hertz, shown in the unit that fits (800 MHz, 3.6 GHz).

use Processor from "std/os"

const threads = Processor.logicalCount().expect("the logical processors")
const cores = Processor.physicalCount().expect("the cores")
print "{threads} threads on {cores} cores"
const usage = Processor.usage(250.milliseconds()).await().expect("the usage")
print "{usage.busy * 100.0}% busy over {usage.each.length()} processors"
countstimeswaiting, other
WindowsGetLogicalProcessorInformationEx (Windows.processorCounts)GetSystemTimes; each processor from NtQuerySystemInformation(SystemProcessorPerformanceInformation)zero: the interrupt and deferred-procedure time is inside system
Linuxsysconf(_SC_NPROCESSORS_ONLN); the cores from the physical id and core id pairs of /proc/cpuinfothe cpu and cpu<n> lines of /proc/stat, in clock ticks of _SC_CLK_TCKwaiting is iowait; other holds nice, interrupts and stolen time
macOShw.logicalcpu, hw.physicalcpuhost_statistics(HOST_CPU_LOAD_INFO); each processor from host_processor_info, in ticks of CLK_TCKother is nice; waiting is zero

FreeBSD answers Unsupported until its slice, and so does the browser, which cannot tell. A /proc/stat that is not in the form Linux writes it is OsError.Malformed. Arm Linux writes no core identifiers in /proc/cpuinfo, so every logical processor counts as a core there. What this process may use of them is CurrentProcess.processors().

What a processor says about itself has three types of its own:

type ProcessorDescription with Show, Equals {
  vendor: String
  model: String
  packages: Int
  physicalCores: Int
  logicalProcessors: Int
  baseFrequency: Frequency?
  maximumFrequency: Frequency?
  caches: List<ProcessorCache>
  features: Set<ProcessorFeature>
}
type ProcessorCache with Show, Equals {
  level: Int
  kind: "data" | "instruction" | "unified"
  size: ByteSize
  lineSize: ByteSize
}
type ProcessorFeature with Show, Equals, Hash {
  case Sse2
  case Sse3
  case Ssse3
  case Sse41
  case Sse42
  case Avx
  case Avx2
  case Fma
  case Bmi1
  case Bmi2
  case Avx512Foundation
  case Aes
  case Sha
  case Neon
  case Crc32
  case DotProduct
  case Sve
  case Sve2
}

A ProcessorCache is one cache the processor lists - its level, whether it holds data, instructions or both, its size and its line size - and a cache that several cores share is one entry with its whole size. A ProcessorFeature is an instruction-set extension a program may dispatch on, named as its vendor spells it. The list is closed and grows with its users, and the Arm cases are declared before anything fills them. Both are what the decoders of std/os/x86 answer today, and what Processor will describe next; neither reads the machine by itself. On x86-64 a feature is listed only where the processor has it and the operating system saves its registers, so Avx, Avx2, Fma and Avx512Foundation are never reported on a system that would corrupt a program using them.

Processor.describe() puts them together with the vendor and the model: the vendor string (GenuineIntel, AuthenticAMD), the model (the brand string, trimmed; the vendor string where the processor has none), the caches and the features. On x86-64 they come from cpuid on Windows, Linux and macOS alike, and the counts from the system, because on a processor with cores of different kinds cpuid answers for the core it happens to run on: logicalProcessors is Processor.logicalCount(), physicalCores is Processor.physicalCount(), and packages the sockets the system names - GetLogicalProcessorInformationEx on Windows, hw.packages on macOS, the distinct physical id values of /proc/cpuinfo on Linux, one where it names none - and the dump's own topology where the system names none. baseFrequency and maximumFrequency are None until a source for them exists. FreeBSD and the browser answer OsError.Unsupported, and so does every architecture other than x86-64, until its description is built.

use Processor, ProcessorFeature from "std/os"

const processor = Processor.describe().expect("the processor")
print "{processor.model} ({processor.vendor})"
print "{processor.packages} package, {processor.physicalCores} cores, {processor.logicalProcessors} threads"
for cache in processor.caches {
  print "L{cache.level} {cache.kind}: {cache.size}"
}
print "AVX2: {processor.features.contains(ProcessorFeature.Avx2)}"

CurrentProcess#

native type CurrentProcess {
  static fn identifier(): Int
  static fn parentIdentifier(): Result<Int, OsError>
  static fn executable(): Result<Path, OsError>
  static fn workingDirectory(): Result<Path, OsError>
  static fn uptime(): Duration
  static fn resources(): Result<ProcessResources, OsError>
  static fn processors(): Result<Int, OsError>
  static fn memoryLimit(): Result<ByteSize?, OsError>
}
type ProcessResources with Show, Equals {
  userTime: Duration
  systemTime: Duration
  resident: ByteSize
  peakResident: ByteSize
}

What the operating system knows about this process: its identifier and its parent's, the executable it runs, the directory a relative path is resolved against, and how long it has been running. What the process does - its arguments, exit, the children it starts - is Process of std/process.

use CurrentProcess, OsError from "std/os"

fn main(): Result<Void, OsError> {
  const directory = CurrentProcess.workingDirectory()?
  const executable = CurrentProcess.executable()?
  print "process {CurrentProcess.identifier()} in {directory}"
  print "running {executable} for {CurrentProcess.uptime()}"
  Ok void
}
identifier, parentIdentifierexecutable
WindowsGetCurrentProcessId, the parent from a CreateToolhelp32Snapshot of the processesGetModuleFileNameW
Linuxgetpid, getppid/proc/self/exe
macOSgetpid, getppid_NSGetExecutablePath, then realpath
FreeBSDgetpid, getppidthe sysctl KERN_PROC_PATHNAME

identifier() always answers; in the browser, where a page is no process of the machine, it is 0, the identifier no process has, and parentIdentifier() is Unsupported. A parent the system does not know is Missing: on Linux the parent of a container's first process, which is outside its namespace, and on Windows a parent that has already ended. executable() is absolute with / between its parts; inside torb run it is the entry file of the program, which is what executes - or, for a program that names Resource files, the staged name the VM runs it as, beside which its .resources directory is written. It is how a program finds the files it was installed beside: torb finds its std/ and runtime/ this way. workingDirectory() is always what File.absolutePath(".") of std/fs answers, because both read it through the same function of the runtime, inside a sandbox too. There is no setWorkingDirectory: the working directory is shared by every task of the process, so a child is given one when it is started instead. uptime() is read on the monotonic clock from before main started, in whole milliseconds and rounded up.

resources() is what the process has used so far: its processor time in user and in system mode, as whole milliseconds cut off, and its resident memory now and at its peak, as ByteSizes; the peak is never less than the resident size. processors() is how many processors the process may keep busy at once - the number to size a pool of workers by, never less than one - and memoryLimit() the most memory it may hold before the system stops it, or None where only the machine limits it.

use CurrentProcess, OsError from "std/os"

fn main(): Result<Void, OsError> {
  const used = CurrentProcess.resources()?
  print "{used.userTime} in user mode, {used.resident} resident, at most {used.peakResident}"
  const processors = CurrentProcess.processors()?
  print "{processors} processors"
  match CurrentProcess.memoryLimit()? {
    Some(limit) => print "held to {limit}"
    None => print "no memory limit"
  }
  Ok void
}
resourcesprocessorsmemoryLimit
WindowsGetProcessTimes, the working set of GetProcessMemoryInfothe affinity mask, every processor group where it is not narrowed, a job object's CPU ratethe memory limit of the job object
Linuxgetrusage, VmRSS of /proc/self/statussched_getaffinity, the cgroups' cpu.max (version 1: cpu.cfs_quota_us) rounded upthe lowest memory.max (version 1: memory.limit_in_bytes) of the cgroups
macOSgetrusage, task_info(MACH_TASK_BASIC_INFO)hw.ncpuNone: macOS has no such limit

getrusage reports the peak in kibibytes on Linux and in bytes on macOS, and resources() converts it. The cgroups are those /proc/self/cgroup names, from the process's own up to the root, so a container's --cpus and --memory count; a cgroup file that is not there, or a /sys the process may not read, is no limit and answers Ok(None), not an error. A dev binary of torb runs in a job object with a memory limit on Windows, which memoryLimit() answers. FreeBSD answers Unsupported for all three until its slice, and so does the browser. Inside torb run the answers are those of the torb process that runs the program.

Volume#

type Volume with Show, Equals {
  mountPoint: Path
  device: String
  fileSystem: String
  label: String?
  kind: VolumeKind
  readOnly: Bool
  fn isRemovable(): Bool
  static fn all(includeVirtual: Bool = false): Result<List<Volume>, OsError>
  fn space(): Task<Result<VolumeSpace, OsError>>
  static fn containing(path: Path): Task<Result<Volume, OsError>>
  fn isVirtual(): Bool
}
type VolumeKind with Show, Equals {
  case Fixed
  case Removable
  case Network
  case Optical
  case Memory
  case Virtual
  case Unknown
}
type VolumeSpace with Show, Equals {
  total: ByteSize
  free: ByteSize
  available: ByteSize
  fn used(): ByteSize
}

Volume.all() is the mount table of the system: one Volume per mount point, with the device, the file system as the system names it, the label where the system knows one, a VolumeKind and whether it is read-only. It never waits, because it asks no file system anything. The virtual volumes - proc, sysfs, devfs - are left out unless includeVirtual: true asks for them. A volume mounted in two places is two values. space() asks the file system how big it is: total, free and available, which is what this user may still write and can be less than free; used() is total - free. Volume.containing(path) answers the volume a path lives on: a relative path is resolved against the working directory, . and .. are folded away, the path is asked about (so a path that does not exist is OsError.Missing), and the mount point that is the longest prefix of it wins. A link is not followed, so a link into another volume belongs to the volume of the link.

use Volume from "std/os"

for volume in Volume.all().expect("the volumes") {
  const space = volume.space().await().expect("the space")
  print "{volume.mountPoint} {volume.fileSystem} {space.used().gigabytes()} of {space.total.gigabytes()} GB"
}
the mount tablekindthe space
WindowsFindFirstVolumeW, GetVolumePathNamesForVolumeNameW; the file system, label and read-only flag from GetVolumeInformationWGetDriveTypeW (removable, fixed, remote, optical, RAM disk)GetDiskFreeSpaceExW
Linux/proc/self/mountinfo, octal escapes (\040) undonethe file system type; a disk whose /sys/block/<disk>/removable is 1 is removablestatvfs
macOSgetmntinfoMNT_LOCAL, MNT_REMOVABLE and the file system typestatvfs

FreeBSD answers Unsupported until its slice, and so does the browser, which has no file systems. A line of /proc/self/mountinfo that is not in the form Linux writes it is OsError.Malformed. On Windows a drive with nothing in it, such as a card reader with no card, and a volume with no path are left out of all(), and no dialog opens.

Directories#

type Directories {
  static fn home(): Result<Path, OsError>
  static fn configuration(): Result<Path, OsError>
  static fn data(): Result<Path, OsError>
  static fn state(): Result<Path, OsError>
  static fn cache(): Result<Path, OsError>
  static fn documents(): Result<Path, OsError>
  static fn downloads(): Result<Path, OsError>
  static fn desktop(): Result<Path, OsError>
  static fn pictures(): Result<Path, OsError>
  static fn music(): Result<Path, OsError>
  static fn videos(): Result<Path, OsError>
  static fn temporary(): Path
}

Where a user's files belong, by the convention of the system. Each answers the base, and an application joins its own name to it: one join, the same on every system.

Linux, FreeBSD (XDG)Windows (Known Folders)macOS
home$HOME, else the account entryFOLDERID_Profile$HOME, else the account entry
configuration$XDG_CONFIG_HOME, else ~/.configFOLDERID_RoamingAppData~/Library/Application Support
data$XDG_DATA_HOME, else ~/.local/shareFOLDERID_RoamingAppData~/Library/Application Support
state$XDG_STATE_HOME, else ~/.local/stateFOLDERID_LocalAppData~/Library/Application Support
cache$XDG_CACHE_HOME, else ~/.cacheFOLDERID_LocalAppData~/Library/Caches
temporary$TMPDIR, else /tmpGetTempPath2W$TMPDIR, else the user's own
documents, downloads, desktop, pictures, music, videosXDG_DOCUMENTS_DIR and its siblings in user-dirs.dirs of the configuration directory, else ~/Documents, ~/Downloads, ~/Desktop, ~/Pictures, ~/Music, ~/VideosFOLDERID_Documents, FOLDERID_Downloads, FOLDERID_Desktop, FOLDERID_Pictures, FOLDERID_Music, FOLDERID_Videos~/Documents, ~/Downloads, ~/Desktop, ~/Pictures, ~/Music, ~/Movies

An XDG variable that holds a relative path is ignored, as the XDG specification requires. temporary() always answers, because every system has a temporary directory and /tmp is the last resort. In the browser, whose file system is in memory and belongs to no user, every directory but temporary() (/tmp) is Unsupported.

The user folders are where a person looks for their files, not where an application keeps its own. A folder answers a path whether or not it exists - the fallback below the home is not checked, so create it before writing to it. A folder the user disabled in user-dirs.dirs (XDG_MUSIC_DIR="$HOME/") answers the fallback, never the home directory itself. On Linux the file is read with File.readText of std/fs; a missing or unreadable one is the fallback, not an error.

use Directories, OsError from "std/os"
use Path from "std/path"

fn main(): Result<Void, OsError> {
  const downloads = Directories.downloads()?
  print downloads.joined(Path.from("report.pdf"))
  Ok void
}

User#

type User with Show, Equals {
  name: String
  fullName: String?
  identifier: String
  home: Path
  isElevated: Bool
  static fn current(): Task<Result<User, OsError>>
}

User.current() answers the account the process runs as. It is a task, because the account database may be a directory service on the network. name is the login name (ada), on Windows without its domain; fullName is the name for people and None where the account has none; identifier is text, because the two kinds have nothing else in common - 1000 on POSIX, the security identifier S-1-5-21-… on Windows; home is the account's own home directory; isElevated says whether the process is the superuser or has an elevated token.

name, home, identifierfullNameisElevated
Linux, macOS, FreeBSDthe account entry of the user id (getpwuid_r); the user id as textthe GECOS field up to its first comma, None where it is emptythe effective user id is 0 (geteuid)
WindowsGetUserNameExW(NameSamCompatible) without the domain; FOLDERID_Profile; the SID of the process token (ConvertSidToStringSidW)GetUserNameExW(NameDisplay), None where the account has none or the domain cannot be reachedTokenElevation of the process token

A browser has no user and answers OsError.Unsupported. A user id with no account entry is OsError.Missing. home is the account's, so it differs from Directories.home() where the HOME variable overrides it. On Windows a member of the administrators group whose program was not started as administrator is not elevated.

use User from "std/os"

const user = User.current().await().expect("the user")
print "{user.name} ({user.identifier}) lives in {user.home}"
if user.isElevated {
  print "running with elevated rights"
}

Entropy#

type Entropy {
  static fn bytes(count: Int): Bytes
  static fn int64(): Int64
}

Bytes of the operating system's source of randomness, which is fit for cryptography: what a key, a nonce, a token and the seed of a generator are made from. bytes(count) answers that many, int64() eight of them as one number whose 64 bits are all random - the unpredictable seed of a generator. Nothing seeds it and nothing repeats it; a generator that makes the same numbers from the same seed, for a test or a simulation, is a different thing (docs/design/RANDOM.md), and this is where its seed comes from, never what it is.

SystemSource
WindowsBCryptGenRandom, the system's preferred generator
Linux, FreeBSDgetrandom(2), which needs no file and so works in a chroot without /dev
macOSgetentropy
the browsercrypto.getRandomValues of the page, through emscripten's getentropy
use Entropy from "std/os/entropy"

const nonce = Entropy.bytes 18
print nonce.length()

It lives in its own module, std/os/entropy, and is a capability of its own, entropy: it reads nothing about the machine, so a package that makes a nonce is reported as touching entropy rather than the operating system (see torb publish), whether it imports Entropy from std/os/entropy or from std/os. A sandboxed script needs the module std/os/entropy granted, and that is the import to write where a script uses it.

It panics where the system does not answer, and bytes where count is negative. None of the systems above fails once it has started, there is no safe way on without randomness, and a failure a caller could handle would invite a weaker source in its place - which is why Go's crypto/rand decided the same.

MacAddress#

type MacAddress with Show, Equals, Hash, TryFrom<String, AddressError> {
  static fn of(octets: List<Int>): Result<Self, AddressError>
  fn octets(): List<Int>
  fn show(): String
  static fn tryFrom(text: String): Result<Self, AddressError>
}

The hardware address of a network interface: six octets. It is a value like Ipv4Address and reads nothing from the machine - what asks the machine for the address of an interface is a later slice of docs/design/OS.md - so it lives in a module of its own, std/os/macaddress, and is no capability: a package that only names a MAC address is not reported as touching the operating system, whether it imports the name from std/os or from std/os/macaddress. It is not in std/ip because it is a link-layer address and std/ip holds nothing that is not IP; AddressError is the error of both and is imported from std/ip.

MacAddress is a capsule: of(octets) takes six numbers from 0 to 255, most significant first, and refuses any other count and an octet outside the range; octets() answers them again. tryFrom reads six groups of exactly two hexadecimal digits, in either case, with one separator throughout, : or - (Windows writes 3C-22-FB-0A-1B-9E). It refuses five or seven groups, a group of one or three digits, mixed separators and any other character. show() is lower case with colons, 3c:22:fb:0a:1b:9e, and tryFrom(show()) gives the value back.

use MacAddress from "std/os"
use AddressError from "std/ip"

fn main(): Result<Void, AddressError> {
  const address = MacAddress.tryFrom("3C-22-FB-0A-1B-9E")?
  print address
  print address.octets()
  Ok void
}

NetworkInterface#

type NetworkInterface with Show, Equals {
  name: String
  description: String?
  index: Int
  hardwareAddress: MacAddress?
  addresses: List<InterfaceAddress>
  mtu: Int
  status: InterfaceStatus
  kind: InterfaceKind
  static fn all(): Result<List<Self>, OsError>
}

A network interface of the machine with its addresses. NetworkInterface.all() lists them in the order of their indexes and asks the system once. On Linux it reads getifaddrs for the addresses and the hardware address and /sys/class/net/<name>/ for the MTU, the state and the kind; a file that is not there leaves mtu at 0, status Unknown and kind Other. description is None on Linux. Every other system answers OsError.Unsupported for now.

status is Up or Down as the system says it, and Unknown otherwise: an interface that is being brought up, or one whose state the driver does not report (a loopback says unknown on most kernels). kind is Loopback for lo, Wireless for an interface with a wireless directory, Tunnel for a tunnel type or a point-to-point link, and Ethernet for an Ethernet one.

use NetworkInterface, OsError from "std/os"

fn main(): Result<Void, OsError> {
  for interface in NetworkInterface.all()? {
    print "{interface.name} {interface.kind} {interface.status} {interface.addresses}"
  }
  Ok void
}

InterfaceAddress#

type InterfaceAddress with Show, Equals {
  address: IpAddress
  prefixLength: Int
  fn show(): String
  fn subnet(): Result<Subnet, OsError>
}

One address of an interface with the length of its network prefix, shown as 192.168.1.23/24. The address holds no zone. subnet() is the network the address is in, 192.168.1.0/24; it is an OsError.Malformed for a prefix length below 0 or above 32 (IPv4) or 128 (IPv6), which the system never writes and a value built by hand can.

use InterfaceAddress, NetworkInterface, OsError from "std/os"

fn main(): Result<Void, OsError> {
  for interface in NetworkInterface.all()? {
    for address in interface.addresses {
      print "{address} is in {address.subnet()?}"
    }
  }
  Ok void
}

InterfaceStatus and InterfaceKind#

type InterfaceStatus with Show, Equals {
  case Up
  case Down
  case Unknown
}
type InterfaceKind with Show, Equals {
  case Ethernet
  case Wireless
  case Loopback
  case Tunnel
  case Other
}

Unknown is not Down: the system did not say. Other is anything the kinds above do not name.

OsError#

type OsError with Show, Equals, Error {
  case Unsupported(question: String, operatingSystem: OperatingSystem)
  case Denied(question: String, reason: String)
  case Missing(question: String, reason: String)
  case Failed(question: String, message: String)
  case Malformed(source: String, detail: String)
  fn show(): String
}

What asking the operating system went wrong with. Unsupported is a question the system cannot answer at all; Denied a refusal (a permission, a container that hides /proc); Missing a question without an answer on this machine (no HOME and no account entry); Failed the system's own words for anything else; Malformed an answer in a form its reader did not expect. No error code of a system is carried, because a portable caller that could read one would compare it. A cancelled question is no case of it: await() passes a cancellation on to the waiting task instead of answering it (see std/task).

The per-system modules#

Each directory of std/os/src/ holds what one system, or one standard several share, has: windows/, linux/, macos/, freebsd/ and browser/ (which has no natives), and posix/, bsd/ (the sysctl interface of macOS and FreeBSD), xdg/ and x86/ (the cpuid of an x86-64 processor, which is the same on every system, and the pure decoders of what it answers: vendor, brand, features, caches and topology). Their native.trb modules are the raw natives of runtime/os/, and a program that needs one imports it directly (use Windows from "std/os/windows/native") and reaches it from the arm of its system; torb check --every-target reports a native that is reached on a system it does not exist on. They are not the documented surface of the package.

  • std/core - OperatingSystem, Architecture and ByteOrder, the constants a branch on the target reads.
  • Compile-time branches - the rule that keeps one arm of a match on the target.
  • std/sandbox - SandboxCapabilities.environment, which grants the patterns a script may read.
  • std/path - Path, what Directories, Environment.searchPath and CurrentProcess answer.
  • std/process - Process, the arguments, exit and the children of the current process.
  • std/number - ByteSize, what Memory, Swap and System.pageSize answer.
  • The standard library - the other packages.
Esc