mirror of
https://github.com/devmobasa/wayscriber.git
synced 2026-11-01 07:36:42 +01:00
Merge pull request #427 from devmobasa/chore/dependency-hardening
Harden dependencies and move kernel calls to rustix
This commit is contained in:
68 files changed
+1299
-1806
No files matched your search
@@ -16,6 +16,8 @@
|
|||||||
binary points at that page. Renaming or removing an id silently drops the
|
binary points at that page. Renaming or removing an id silently drops the
|
||||||
prefill for binaries that can never be updated retroactively.
|
prefill for binaries that can never be updated retroactively.
|
||||||
- Keep Linux system dependencies aligned with real build needs for Wayland, Cairo, Pango, GTK4/libadwaita, D-Bus, packaging, and Nix checks.
|
- Keep Linux system dependencies aligned with real build needs for Wayland, Cairo, Pango, GTK4/libadwaita, D-Bus, packaging, and Nix checks.
|
||||||
|
- `deny.toml` is the dependency policy and the `dependency-policy` job enforces it. Advisories report without blocking, so a new advisory cannot fail an unrelated pull request; bans, licenses, and sources block.
|
||||||
|
- `dependabot.yml` proposes weekly Cargo and GitHub Actions updates. Its zbus major-version ignore is coupled to the advisory ignores in `deny.toml`.
|
||||||
- Do not weaken release checks or skip package layout/version validation without a documented reason.
|
- Do not weaken release checks or skip package layout/version validation without a documented reason.
|
||||||
|
|
||||||
## Coupled Changes
|
## Coupled Changes
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: cargo
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
groups:
|
||||||
|
compatible:
|
||||||
|
update-types:
|
||||||
|
- minor
|
||||||
|
- patch
|
||||||
|
ignore:
|
||||||
|
# ksni, which the tray feature needs, still requires zbus 5. Remove this
|
||||||
|
# with the matching advisory ignores in deny.toml.
|
||||||
|
- dependency-name: zbus
|
||||||
|
update-types:
|
||||||
|
- version-update:semver-major
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
@@ -144,3 +144,26 @@ jobs:
|
|||||||
|
|
||||||
- name: Check flake package and development shell instantiation
|
- name: Check flake package and development shell instantiation
|
||||||
run: dotnet run tools/wayscriber.cs --no-build -- check nix-instantiation
|
run: dotnet run tools/wayscriber.cs --no-build -- check nix-instantiation
|
||||||
|
|
||||||
|
dependency-policy:
|
||||||
|
name: Dependency policy (${{ matrix.checks }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
checks:
|
||||||
|
- advisories
|
||||||
|
- bans licenses sources
|
||||||
|
# A newly published advisory must not block unrelated pull requests.
|
||||||
|
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check dependency policy
|
||||||
|
uses: EmbarkStudios/cargo-deny-action@v2
|
||||||
|
with:
|
||||||
|
arguments: --workspace --locked
|
||||||
|
command: check ${{ matrix.checks }}
|
||||||
|
rust-version: 1.99.1
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
- Never reach inside a poisoned lock. Use `expect` when the critical section cannot panic, or a fallback that leaves the data alone. Tests that serialize on process-wide state use `test_env::lock()` or a `test_env::Lease`.
|
- Never reach inside a poisoned lock. Use `expect` when the critical section cannot panic, or a fallback that leaves the data alone. Tests that serialize on process-wide state use `test_env::lock()` or a `test_env::Lease`.
|
||||||
- `catch_unwind` is reserved for the reviewed worker-isolation sites that carry an `#[expect]` reason. Tests observe panics through a joined scoped thread.
|
- `catch_unwind` is reserved for the reviewed worker-isolation sites that carry an `#[expect]` reason. Tests observe panics through a joined scoped thread.
|
||||||
- Getters are `foo()`/`foo_mut()`, a fallible constructor is `try_new`, and type lifetimes have descriptive names. Every `unsafe` block has a `// SAFETY:` comment that says why it is sound.
|
- Getters are `foo()`/`foo_mut()`, a fallible constructor is `try_new`, and type lifetimes have descriptive names. Every `unsafe` block has a `// SAFETY:` comment that says why it is sound.
|
||||||
|
- Make kernel calls through `std` or `rustix`, which need no `unsafe`, and rename on import when a module name clashes with `std` (`use rustix::fs as rustix_fs`). Use raw `libc` calls only where neither fits: signal handlers, the post-`clone` broker stub, `fail_stop`, the zero-signal pidfd check, validating the broker's inherited descriptors, the inotify header parser, `dup2` onto a fixed descriptor number, and `localtime_r`. `libc` constants stay fine.
|
||||||
- Convert numbers losslessly with `From` (`f64::from(width)`) wherever it can be called; `const` initializers keep `as`. Take the middle of two values with `midpoint` (`f64::midpoint(a, b)`), which cannot overflow.
|
- Convert numbers losslessly with `From` (`f64::from(width)`) wherever it can be called; `const` initializers keep `as`. Take the middle of two values with `midpoint` (`f64::midpoint(a, b)`), which cannot overflow.
|
||||||
- Inline named format arguments: `format!("{name}: {err}")`.
|
- Inline named format arguments: `format!("{name}: {err}")`.
|
||||||
- Use `map_or`, `map_or_else`, `is_some_and`, or `is_ok_and` instead of `map(..).unwrap_or(..)`. Use `let ... else` for a `match` that only unwraps or leaves, and `if let ... else` for a `match` with one pattern and a fallback.
|
- Use `map_or`, `map_or_else`, `is_some_and`, or `is_ok_and` instead of `map(..).unwrap_or(..)`. Use `let ... else` for a `match` that only unwraps or leaves, and `if let ... else` for a `match` with one pattern and a fallback.
|
||||||
@@ -51,8 +52,10 @@
|
|||||||
- Config changes often require `config.example.toml`, `docs/CONFIG.md`, configurator models/views, schema behavior, and tests.
|
- Config changes often require `config.example.toml`, `docs/CONFIG.md`, configurator models/views, schema behavior, and tests.
|
||||||
- Tool/action/keybinding changes often require config defaults, action metadata, help/command UI, toolbar UI, configurator labels/search, docs, and tests.
|
- Tool/action/keybinding changes often require config defaults, action metadata, help/command UI, toolbar UI, configurator labels/search, docs, and tests.
|
||||||
- Daemon, service, shortcut, path, and packaging changes often need updates across `src/daemon/`, `src/paths/`, `src/systemd_user_service.rs`, `src/shortcut_hint.rs`, `configurator/src/app/daemon_setup/`, and `packaging/`.
|
- Daemon, service, shortcut, path, and packaging changes often need updates across `src/daemon/`, `src/paths/`, `src/systemd_user_service.rs`, `src/shortcut_hint.rs`, `configurator/src/app/daemon_setup/`, and `packaging/`.
|
||||||
|
- Dependency changes must satisfy `deny.toml`. A new license or duplicate version needs an entry there with its reason.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
- For a fast full-suite run while iterating, use `cargo nextest run --workspace --all-features`. Each test gets its own process, so it runs in parallel despite the native-font race that keeps the gate serial. It skips doctests and the isolated render tests, so it does not replace the gate.
|
||||||
- Full local CI is `./tools/lint-and-test.sh`. It needs the .NET SDK selected by `global.json`. Without it, `cargo test` still runs the Rust source guards in `tests/repository_guards` (process sites, config writers, shared dependencies, use statements, no Python), but not the C# checks, including the source-coverage check that finds `.rs` files Cargo never compiles.
|
- Full local CI is `./tools/lint-and-test.sh`. It needs the .NET SDK selected by `global.json`. Without it, `cargo test` still runs the Rust source guards in `tests/repository_guards` (process sites, config writers, shared dependencies, use statements, no Python), but not the C# checks, including the source-coverage check that finds `.rs` files Cargo never compiles.
|
||||||
- That script runs the C# repository checks (assets, version, nixpkgs recipe, source coverage, legacy tools), the C# tool tests (which also run the packaging shell contracts), `cargo fmt --all -- --check`, clippy with all targets/features, all-feature tests, and no-default-feature tests.
|
- That script runs the C# repository checks (assets, version, nixpkgs recipe, source coverage, legacy tools), the C# tool tests (which also run the packaging shell contracts), `cargo fmt --all -- --check`, clippy with all targets/features, all-feature tests, and no-default-feature tests.
|
||||||
- Run `git diff --check` before handoff. When relevant files are untracked and the index must stay unchanged, check those files directly as well.
|
- Run `git diff --check` before handoff. When relevant files are untracked and the index must stay unchanged, check those files directly as well.
|
||||||
|
|||||||
@@ -125,6 +125,16 @@ cargo test session::
|
|||||||
cargo test -p wayscriber-configurator
|
cargo test -p wayscriber-configurator
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a fast run of the whole suite, use [cargo-nextest](https://nexte.st/), which the Nix shell
|
||||||
|
includes. It gives each test its own process, so it can run in parallel despite the native font
|
||||||
|
race that keeps the local CI entry point serial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo nextest run --workspace --all-features
|
||||||
|
```
|
||||||
|
|
||||||
|
It skips doctests and the isolated render tests, so it does not replace that entry point.
|
||||||
|
|
||||||
Before submitting a broad or cross-package change, run the local CI entry point:
|
Before submitting a broad or cross-package change, run the local CI entry point:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -139,6 +149,13 @@ Rust tests include the repository guards in `tests/repository_guards`. The sourc
|
|||||||
gate uses current rustc dep-info and rejects tracked or unignored `.rs` files that are outside
|
gate uses current rustc dep-info and rejects tracked or unignored `.rs` files that are outside
|
||||||
the supported Cargo target/feature matrix.
|
the supported Cargo target/feature matrix.
|
||||||
|
|
||||||
|
Dependency changes must also satisfy the policy in `deny.toml`, which CI enforces in its own
|
||||||
|
job. The Nix shell includes `cargo-deny`; elsewhere, install it and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo deny --workspace --locked check
|
||||||
|
```
|
||||||
|
|
||||||
The all-feature portal transport tests require `dbus-daemon`. Each fixture owns a private
|
The all-feature portal transport tests require `dbus-daemon`. Each fixture owns a private
|
||||||
session bus and connects through its explicit address, leaving the desktop session bus alone.
|
session bus and connects through its explicit address, leaving the desktop session bus alone.
|
||||||
|
|
||||||
|
|||||||
Generated
+12
-37
@@ -294,7 +294,7 @@ version = "2.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.4.3",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -513,18 +513,6 @@ dependencies = [
|
|||||||
"system-deps 9.0.1",
|
"system-deps 9.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "getrandom"
|
|
||||||
version = "0.3.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"r-efi 5.3.0",
|
|
||||||
"wasip2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -534,7 +522,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 6.0.0",
|
"r-efi",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -882,6 +870,12 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lexopt"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "803ec87c9cfb29b9d2633f20cba1f488db3fd53f2158b1024cbefb47ba05d413"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libadwaita"
|
name = "libadwaita"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
@@ -1155,12 +1149,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "r-efi"
|
|
||||||
version = "5.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r-efi"
|
name = "r-efi"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -1528,7 +1516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.4.3",
|
"getrandom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -1724,15 +1712,6 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasip2"
|
|
||||||
version = "1.0.4+wasi-0.2.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
|
||||||
dependencies = [
|
|
||||||
"wit-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.129"
|
version = "0.2.129"
|
||||||
@@ -1903,17 +1882,19 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"cairo-rs",
|
"cairo-rs",
|
||||||
"flate2",
|
"flate2",
|
||||||
"getrandom 0.3.4",
|
"getrandom",
|
||||||
"glib",
|
"glib",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-layer-shell",
|
"gtk4-layer-shell",
|
||||||
"input",
|
"input",
|
||||||
"ksni",
|
"ksni",
|
||||||
|
"lexopt",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"pango",
|
"pango",
|
||||||
"pangocairo",
|
"pangocairo",
|
||||||
"png",
|
"png",
|
||||||
|
"rustix",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_ignored",
|
"serde_ignored",
|
||||||
@@ -2035,12 +2016,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen"
|
|
||||||
version = "0.57.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xcursor"
|
name = "xcursor"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
|
|||||||
+8
-2
@@ -3,6 +3,7 @@ name = "wayscriber"
|
|||||||
version = "0.9.28"
|
version = "0.9.28"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.99.1"
|
rust-version = "1.99.1"
|
||||||
|
license = "MIT"
|
||||||
description = "Screen annotation tool for Wayland compositors"
|
description = "Screen annotation tool for Wayland compositors"
|
||||||
homepage = "https://wayscriber.com"
|
homepage = "https://wayscriber.com"
|
||||||
repository = "https://github.com/devmobasa/wayscriber"
|
repository = "https://github.com/devmobasa/wayscriber"
|
||||||
@@ -92,10 +93,16 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_ignored = "0.1"
|
serde_ignored = "0.1"
|
||||||
schemars = { version = "1.2", features = ["derive"], optional = true }
|
schemars = { version = "1.2", features = ["derive"], optional = true }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
# Safe wrappers for the kernel interfaces that the std library does not cover.
|
||||||
|
rustix = { version = "1", features = ["event", "fs", "net", "pipe", "process", "thread", "time"] }
|
||||||
|
# Kernel random bytes for portal handle tokens and daemon protocol identifiers.
|
||||||
|
getrandom = { version = "0.4", features = ["std"] }
|
||||||
# Securely created, self-deleting temporary files for the OCR engine handoff.
|
# Securely created, self-deleting temporary files for the OCR engine handoff.
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
flate2 = "1.0"
|
flate2 = "1.0"
|
||||||
zune-jpeg = "0.5"
|
zune-jpeg = "0.5"
|
||||||
|
# Command-line tokenizing without a parser framework.
|
||||||
|
lexopt = "0.3"
|
||||||
# Grapheme/word segmentation for the text-editor caret and selection.
|
# Grapheme/word segmentation for the text-editor caret and selection.
|
||||||
unicode-segmentation = "1"
|
unicode-segmentation = "1"
|
||||||
|
|
||||||
@@ -105,7 +112,6 @@ tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "time", "syn
|
|||||||
|
|
||||||
# Screenshot capture
|
# Screenshot capture
|
||||||
zbus = { version = "5.0", optional = true, default-features = false, features = ["tokio"] }
|
zbus = { version = "5.0", optional = true, default-features = false, features = ["tokio"] }
|
||||||
getrandom = { version = "0.3", optional = true }
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
png = "0.18"
|
png = "0.18"
|
||||||
|
|
||||||
@@ -125,7 +131,7 @@ xkbcommon = { version = "0.8", optional = true }
|
|||||||
tablet-input = []
|
tablet-input = []
|
||||||
# D-Bus dependent functionality (portal capture, notifications, tray)
|
# D-Bus dependent functionality (portal capture, notifications, tray)
|
||||||
dbus = ["zbus"]
|
dbus = ["zbus"]
|
||||||
portal = ["dbus", "dep:getrandom"]
|
portal = ["dbus"]
|
||||||
tray = ["dbus", "ksni"]
|
tray = ["dbus", "ksni"]
|
||||||
config-schema = ["dep:schemars"]
|
config-schema = ["dep:schemars"]
|
||||||
# GTK4-rendered toolbars on layer-shell compositors; the built-in Cairo
|
# GTK4-rendered toolbars on layer-shell compositors; the built-in Cairo
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ name = "wayscriber-configurator"
|
|||||||
version = "0.9.28"
|
version = "0.9.28"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.99.1"
|
rust-version = "1.99.1"
|
||||||
|
license = "MIT"
|
||||||
|
publish = false
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Dependency policy for the workspace. CI runs it in the `dependency-policy` job;
|
||||||
|
# `cargo deny --workspace --locked check` gives the same result locally.
|
||||||
|
|
||||||
|
[graph]
|
||||||
|
all-features = true
|
||||||
|
targets = [
|
||||||
|
{ triple = "x86_64-unknown-linux-gnu" },
|
||||||
|
{ triple = "aarch64-unknown-linux-gnu" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[advisories]
|
||||||
|
# zbus 6 absorbed these crates. ksni, which the `tray` feature needs, still
|
||||||
|
# requires zbus 5, so remove these entries once it moves.
|
||||||
|
ignore = [
|
||||||
|
{ id = "RUSTSEC-2026-0370", reason = "zvariant: blocked on ksni supporting zbus 6" },
|
||||||
|
{ id = "RUSTSEC-2026-0371", reason = "zbus_names: blocked on ksni supporting zbus 6" },
|
||||||
|
{ id = "RUSTSEC-2026-0372", reason = "zvariant_derive: blocked on ksni supporting zbus 6" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[licenses]
|
||||||
|
allow = [
|
||||||
|
"Apache-2.0",
|
||||||
|
"Apache-2.0 WITH LLVM-exception",
|
||||||
|
"MIT",
|
||||||
|
"Unicode-3.0",
|
||||||
|
"Unlicense",
|
||||||
|
]
|
||||||
|
|
||||||
|
[bans]
|
||||||
|
multiple-versions = "deny"
|
||||||
|
wildcards = "deny"
|
||||||
|
allow-wildcard-paths = true
|
||||||
|
skip = [
|
||||||
|
{ crate = "miniz_oxide@0.8", reason = "png needs 0.8 while flate2 needs 0.9" },
|
||||||
|
{ crate = "syn@2", reason = "proc macros have not all moved to syn 3" },
|
||||||
|
{ crate = "system-deps@7", reason = "build-only: the GTK sys crates span three majors" },
|
||||||
|
{ crate = "system-deps@8", reason = "build-only: the GTK sys crates span three majors" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[sources]
|
||||||
|
unknown-registry = "deny"
|
||||||
|
unknown-git = "deny"
|
||||||
@@ -32,6 +32,8 @@
|
|||||||
servicePath = pkgs.lib.makeBinPath [ pkgs.grim pkgs.slurp pkgs.wl-clipboard ];
|
servicePath = pkgs.lib.makeBinPath [ pkgs.grim pkgs.slurp pkgs.wl-clipboard ];
|
||||||
developmentInputs = with pkgs; [
|
developmentInputs = with pkgs; [
|
||||||
rustToolchain
|
rustToolchain
|
||||||
|
cargo-deny
|
||||||
|
cargo-nextest
|
||||||
util-linux
|
util-linux
|
||||||
pkg-config
|
pkg-config
|
||||||
cairo
|
cairo
|
||||||
|
|||||||
+15
-9
@@ -22,10 +22,14 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use env::env_flag_enabled;
|
use env::env_flag_enabled;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use rustix::process as rustix_process;
|
||||||
use session::run_session_cli_commands;
|
use session::run_session_cli_commands;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::io::IsTerminal;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use usage::{log_overlay_controls, print_usage};
|
use usage::{log_overlay_controls, print_usage};
|
||||||
|
|
||||||
@@ -170,16 +174,17 @@ fn run_update_check() -> anyhow::Result<()> {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn detach_from_tty() {
|
fn detach_from_tty() {
|
||||||
// Start a new session to drop the controlling terminal (prevents stuck shells).
|
// Start a new session to drop the controlling terminal (prevents stuck shells).
|
||||||
// SAFETY: setsid takes no arguments; a failure only leaves the session as is.
|
// A failure only leaves the session as is.
|
||||||
unsafe {
|
let _ = rustix_process::setsid();
|
||||||
let _ = libc::setsid();
|
|
||||||
}
|
|
||||||
// Point stdio that still refers to a TTY at /dev/null. Closing it instead
|
// Point stdio that still refers to a TTY at /dev/null. Closing it instead
|
||||||
// frees the descriptor number for the next open, and the logger's stderr
|
// frees the descriptor number for the next open, and the logger's stderr
|
||||||
// writes would then land in whatever file that open returned.
|
// writes would then land in whatever file that open returned.
|
||||||
for fd in [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO] {
|
let stdio = [
|
||||||
// SAFETY: isatty only inspects the descriptor number it is given.
|
(libc::STDIN_FILENO, io::stdin().is_terminal()),
|
||||||
let is_tty = unsafe { libc::isatty(fd) } == 1;
|
(libc::STDOUT_FILENO, io::stdout().is_terminal()),
|
||||||
|
(libc::STDERR_FILENO, io::stderr().is_terminal()),
|
||||||
|
];
|
||||||
|
for (fd, is_tty) in stdio {
|
||||||
if is_tty && let Err(error) = redirect_to_dev_null(fd) {
|
if is_tty && let Err(error) = redirect_to_dev_null(fd) {
|
||||||
log::warn!("Failed to detach descriptor {fd} from the terminal: {error}");
|
log::warn!("Failed to detach descriptor {fd} from the terminal: {error}");
|
||||||
}
|
}
|
||||||
@@ -414,6 +419,8 @@ mod tests {
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::os::fd::AsRawFd;
|
use std::os::fd::AsRawFd;
|
||||||
|
|
||||||
|
use rustix::io as rustix_io;
|
||||||
|
|
||||||
let temp = test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let path = temp.path().join("was-a-terminal");
|
let path = temp.path().join("was-a-terminal");
|
||||||
let mut file = File::create(&path).unwrap();
|
let mut file = File::create(&path).unwrap();
|
||||||
@@ -422,8 +429,7 @@ mod tests {
|
|||||||
redirect_to_dev_null(file.as_raw_fd()).unwrap();
|
redirect_to_dev_null(file.as_raw_fd()).unwrap();
|
||||||
file.write_all(b"after").unwrap();
|
file.write_all(b"after").unwrap();
|
||||||
|
|
||||||
// SAFETY: F_GETFD only inspects the descriptor the file still owns.
|
rustix_io::fcntl_getfd(&file).unwrap();
|
||||||
assert!(unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFD) } >= 0);
|
|
||||||
assert_eq!(fs::read(&path).unwrap(), b"before ");
|
assert_eq!(fs::read(&path).unwrap(), b"before ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,11 +64,8 @@ pub(super) fn dispatch_events(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io;
|
|
||||||
use std::io::ErrorKind;
|
|
||||||
use std::os::fd::AsRawFd;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Instant;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::backend::wayland::session::{
|
use crate::backend::wayland::session::{
|
||||||
@@ -126,27 +123,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_runtime_wake(runtime_wake: &RuntimeWakeSource) {
|
fn wait_for_runtime_wake(runtime_wake: &RuntimeWakeSource) {
|
||||||
let mut pollfd = libc::pollfd {
|
assert!(
|
||||||
fd: runtime_wake.poll_fd().as_raw_fd(),
|
runtime_wake
|
||||||
events: libc::POLLIN,
|
.is_readable_within(Duration::from_secs(1))
|
||||||
revents: 0,
|
.unwrap(),
|
||||||
};
|
"persistence worker did not wake the production route"
|
||||||
loop {
|
);
|
||||||
// SAFETY: pollfd and the runtime wake descriptor stay valid for this
|
|
||||||
// bounded test wait.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 1_000) };
|
|
||||||
if ready > 0 {
|
|
||||||
assert_ne!(pollfd.revents & libc::POLLIN, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ready == 0 {
|
|
||||||
panic!("persistence worker did not wake the production route");
|
|
||||||
}
|
|
||||||
let err = io::Error::last_os_error();
|
|
||||||
if err.kind() != ErrorKind::Interrupted {
|
|
||||||
panic!("runtime wake poll failed: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ use std::io::ErrorKind;
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
use rustix::event::{PollFd, PollFlags};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::os::fd::{AsRawFd, RawFd};
|
use std::os::fd::BorrowedFd;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use wayland_client::EventQueue;
|
use wayland_client::EventQueue;
|
||||||
use wayland_client::backend::{ReadEventsGuard, WaylandError};
|
use wayland_client::backend::{ReadEventsGuard, WaylandError};
|
||||||
@@ -13,7 +14,8 @@ use super::super::state::WaylandState;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use super::runtime_wake::timeout_to_poll_ms;
|
use super::runtime_wake::timeout_to_poll_ms;
|
||||||
use super::runtime_wake::{
|
use super::runtime_wake::{
|
||||||
RuntimeWakeSource, TerminalReadinessPolicy, poll_with_retry, validate_poll_readiness,
|
RuntimeWakeSource, TerminalReadinessPolicy, poll_for_ms, poll_with_retry,
|
||||||
|
validate_poll_readiness,
|
||||||
};
|
};
|
||||||
use crate::capture::CaptureFailureKind;
|
use crate::capture::CaptureFailureKind;
|
||||||
use crate::i18n::tr;
|
use crate::i18n::tr;
|
||||||
@@ -73,13 +75,13 @@ struct RuntimeReadOutcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trait PreparedWaylandRead {
|
trait PreparedWaylandRead {
|
||||||
fn connection_raw_fd(&self) -> RawFd;
|
fn connection_fd(&self) -> BorrowedFd<'_>;
|
||||||
fn read(self) -> Result<usize, WaylandError>;
|
fn read(self) -> Result<usize, WaylandError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreparedWaylandRead for ReadEventsGuard {
|
impl PreparedWaylandRead for ReadEventsGuard {
|
||||||
fn connection_raw_fd(&self) -> RawFd {
|
fn connection_fd(&self) -> BorrowedFd<'_> {
|
||||||
self.connection_fd().as_raw_fd()
|
ReadEventsGuard::connection_fd(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read(self) -> Result<usize, WaylandError> {
|
fn read(self) -> Result<usize, WaylandError> {
|
||||||
@@ -87,52 +89,28 @@ impl PreparedWaylandRead for ReadEventsGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies the retry and readiness rules to `poll_once`, which reports the
|
||||||
|
/// Wayland and runtime wake readiness in that order. Both empty means timeout.
|
||||||
fn poll_runtime_fds_with(
|
fn poll_runtime_fds_with(
|
||||||
wayland_fd: RawFd,
|
|
||||||
wake_fd: RawFd,
|
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
mut poll_once: impl FnMut(&mut [libc::pollfd], i32) -> io::Result<i32>,
|
mut poll_once: impl FnMut(i32) -> io::Result<[PollFlags; 2]>,
|
||||||
) -> io::Result<RuntimePollReadiness> {
|
) -> io::Result<RuntimePollReadiness> {
|
||||||
poll_with_retry(timeout, |timeout_ms| {
|
poll_with_retry(timeout, |timeout_ms| {
|
||||||
let mut pollfds = [
|
let [wayland, wake] = poll_once(timeout_ms)?;
|
||||||
libc::pollfd {
|
if wayland.is_empty() && wake.is_empty() {
|
||||||
fd: wayland_fd,
|
return Ok(None);
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
},
|
|
||||||
libc::pollfd {
|
|
||||||
fd: wake_fd,
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
match poll_once(&mut pollfds, timeout_ms) {
|
|
||||||
Ok(0) => Ok(None),
|
|
||||||
Ok(_) => {
|
|
||||||
let readiness = RuntimePollReadiness {
|
|
||||||
// A Wayland socket can report final buffered protocol data
|
|
||||||
// together with HUP/ERR. Read and dispatch it first so the
|
|
||||||
// compositor's actual disconnect error is preserved.
|
|
||||||
wayland: validate_poll_readiness(
|
|
||||||
&pollfds[0],
|
|
||||||
"Wayland",
|
|
||||||
TerminalReadinessPolicy::ReadBuffered,
|
|
||||||
)?,
|
|
||||||
wake: validate_poll_readiness(
|
|
||||||
&pollfds[1],
|
|
||||||
"runtime wake",
|
|
||||||
TerminalReadinessPolicy::Reject,
|
|
||||||
)?,
|
|
||||||
};
|
|
||||||
if !readiness.wayland && !readiness.wake {
|
|
||||||
return Err(io::Error::other(
|
|
||||||
"runtime poll reported readiness without a readable descriptor",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(Some(readiness))
|
|
||||||
}
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
}
|
||||||
|
Ok(Some(RuntimePollReadiness {
|
||||||
|
// A Wayland socket can report final buffered protocol data
|
||||||
|
// together with HUP/ERR. Read and dispatch it first so the
|
||||||
|
// compositor's actual disconnect error is preserved.
|
||||||
|
wayland: validate_poll_readiness(
|
||||||
|
wayland,
|
||||||
|
"Wayland",
|
||||||
|
TerminalReadinessPolicy::ReadBuffered,
|
||||||
|
)?,
|
||||||
|
wake: validate_poll_readiness(wake, "runtime wake", TerminalReadinessPolicy::Reject)?,
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
.map(|readiness| {
|
.map(|readiness| {
|
||||||
readiness.unwrap_or(RuntimePollReadiness {
|
readiness.unwrap_or(RuntimePollReadiness {
|
||||||
@@ -143,25 +121,17 @@ fn poll_runtime_fds_with(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn poll_runtime_fds(
|
fn poll_runtime_fds(
|
||||||
wayland_fd: RawFd,
|
wayland_fd: BorrowedFd<'_>,
|
||||||
wake_fd: RawFd,
|
wake_fd: BorrowedFd<'_>,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
) -> io::Result<RuntimePollReadiness> {
|
) -> io::Result<RuntimePollReadiness> {
|
||||||
poll_runtime_fds_with(wayland_fd, wake_fd, timeout, |pollfds, timeout_ms| {
|
poll_runtime_fds_with(timeout, |timeout_ms| {
|
||||||
// SAFETY: pollfds is a live mutable slice for the duration of this call,
|
let mut pollfds = [
|
||||||
// and both descriptors remain borrowed by their runtime owners.
|
PollFd::from_borrowed_fd(wayland_fd, PollFlags::IN),
|
||||||
let ready = unsafe {
|
PollFd::from_borrowed_fd(wake_fd, PollFlags::IN),
|
||||||
libc::poll(
|
];
|
||||||
pollfds.as_mut_ptr(),
|
poll_for_ms(&mut pollfds, timeout_ms)?;
|
||||||
pollfds.len() as libc::nfds_t,
|
Ok([pollfds[0].revents(), pollfds[1].revents()])
|
||||||
timeout_ms,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if ready < 0 {
|
|
||||||
Err(io::Error::last_os_error())
|
|
||||||
} else {
|
|
||||||
Ok(ready)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,12 +140,8 @@ fn read_events_with_runtime_wake(
|
|||||||
runtime_wake: &RuntimeWakeSource,
|
runtime_wake: &RuntimeWakeSource,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
) -> Result<RuntimeReadOutcome> {
|
) -> Result<RuntimeReadOutcome> {
|
||||||
let readiness = poll_runtime_fds(
|
let readiness = poll_runtime_fds(guard.connection_fd(), runtime_wake.poll_fd(), timeout)
|
||||||
guard.connection_raw_fd(),
|
.context("runtime readiness poll failed")?;
|
||||||
runtime_wake.poll_fd().as_raw_fd(),
|
|
||||||
timeout,
|
|
||||||
)
|
|
||||||
.context("runtime readiness poll failed")?;
|
|
||||||
|
|
||||||
let wayland_read = if readiness.wayland {
|
let wayland_read = if readiness.wayland {
|
||||||
let _ =
|
let _ =
|
||||||
@@ -331,6 +297,7 @@ mod tests {
|
|||||||
use crate::set_runtime_session_override;
|
use crate::set_runtime_session_override;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::os::fd::AsFd;
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
@@ -372,12 +339,7 @@ mod tests {
|
|||||||
wake.handle().wake().unwrap();
|
wake.handle().wake().unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
poll_runtime_fds(
|
poll_runtime_fds(wayland_read.as_fd(), wake.poll_fd(), Some(Duration::ZERO),).unwrap(),
|
||||||
wayland_read.as_raw_fd(),
|
|
||||||
wake.poll_fd().as_raw_fd(),
|
|
||||||
Some(Duration::ZERO),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
RuntimePollReadiness {
|
RuntimePollReadiness {
|
||||||
wayland: false,
|
wayland: false,
|
||||||
wake: true,
|
wake: true,
|
||||||
@@ -392,12 +354,7 @@ mod tests {
|
|||||||
wayland_write.write_all(&[1]).unwrap();
|
wayland_write.write_all(&[1]).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
poll_runtime_fds(
|
poll_runtime_fds(wayland_read.as_fd(), wake.poll_fd(), Some(Duration::ZERO),).unwrap(),
|
||||||
wayland_read.as_raw_fd(),
|
|
||||||
wake.poll_fd().as_raw_fd(),
|
|
||||||
Some(Duration::ZERO),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
RuntimePollReadiness {
|
RuntimePollReadiness {
|
||||||
wayland: true,
|
wayland: true,
|
||||||
wake: false,
|
wake: false,
|
||||||
@@ -413,12 +370,7 @@ mod tests {
|
|||||||
drop(wayland_write);
|
drop(wayland_write);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
poll_runtime_fds(
|
poll_runtime_fds(wayland_read.as_fd(), wake.poll_fd(), Some(Duration::ZERO),).unwrap(),
|
||||||
wayland_read.as_raw_fd(),
|
|
||||||
wake.poll_fd().as_raw_fd(),
|
|
||||||
Some(Duration::ZERO),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
RuntimePollReadiness {
|
RuntimePollReadiness {
|
||||||
wayland: true,
|
wayland: true,
|
||||||
wake: false,
|
wake: false,
|
||||||
@@ -434,8 +386,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
poll_runtime_fds(
|
poll_runtime_fds(
|
||||||
wayland_read.as_raw_fd(),
|
wayland_read.as_fd(),
|
||||||
wake.poll_fd().as_raw_fd(),
|
wake.poll_fd(),
|
||||||
Some(Duration::from_secs(30)),
|
Some(Duration::from_secs(30)),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
@@ -454,12 +406,7 @@ mod tests {
|
|||||||
wake.handle().wake().unwrap();
|
wake.handle().wake().unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
poll_runtime_fds(
|
poll_runtime_fds(wayland_read.as_fd(), wake.poll_fd(), Some(Duration::ZERO),).unwrap(),
|
||||||
wayland_read.as_raw_fd(),
|
|
||||||
wake.poll_fd().as_raw_fd(),
|
|
||||||
Some(Duration::ZERO),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
RuntimePollReadiness {
|
RuntimePollReadiness {
|
||||||
wayland: true,
|
wayland: true,
|
||||||
wake: true,
|
wake: true,
|
||||||
@@ -475,8 +422,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PreparedWaylandRead for FakePreparedWaylandRead {
|
impl PreparedWaylandRead for FakePreparedWaylandRead {
|
||||||
fn connection_raw_fd(&self) -> RawFd {
|
fn connection_fd(&self) -> BorrowedFd<'_> {
|
||||||
self.stream.as_raw_fd()
|
self.stream.as_fd()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read(mut self) -> Result<usize, WaylandError> {
|
fn read(mut self) -> Result<usize, WaylandError> {
|
||||||
@@ -684,13 +631,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn runtime_poll_retries_interruption() {
|
fn runtime_poll_retries_interruption() {
|
||||||
let mut calls = 0;
|
let mut calls = 0;
|
||||||
let readiness = poll_runtime_fds_with(10, 11, None, |pollfds, _| {
|
let readiness = poll_runtime_fds_with(None, |_| {
|
||||||
calls += 1;
|
calls += 1;
|
||||||
if calls == 1 {
|
if calls == 1 {
|
||||||
return Err(io::Error::from(ErrorKind::Interrupted));
|
return Err(io::Error::from(ErrorKind::Interrupted));
|
||||||
}
|
}
|
||||||
pollfds[1].revents = libc::POLLIN;
|
Ok([PollFlags::empty(), PollFlags::IN])
|
||||||
Ok(1)
|
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -706,11 +652,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_poll_rejects_invalid_descriptor_readiness() {
|
fn runtime_poll_rejects_invalid_descriptor_readiness() {
|
||||||
let err = poll_runtime_fds_with(10, 11, None, |pollfds, _| {
|
let err =
|
||||||
pollfds[1].revents = libc::POLLNVAL;
|
poll_runtime_fds_with(None, |_| Ok([PollFlags::empty(), PollFlags::NVAL])).unwrap_err();
|
||||||
Ok(1)
|
|
||||||
})
|
|
||||||
.unwrap_err();
|
|
||||||
|
|
||||||
assert!(err.to_string().contains("runtime wake"));
|
assert!(err.to_string().contains("runtime wake"));
|
||||||
assert!(err.to_string().contains("readiness"));
|
assert!(err.to_string().contains("readiness"));
|
||||||
@@ -718,9 +661,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_poll_timeout_reports_no_readiness() {
|
fn runtime_poll_timeout_reports_no_readiness() {
|
||||||
let readiness = poll_runtime_fds_with(10, 11, Some(Duration::ZERO), |_, timeout_ms| {
|
let readiness = poll_runtime_fds_with(Some(Duration::ZERO), |timeout_ms| {
|
||||||
assert_eq!(timeout_ms, 0);
|
assert_eq!(timeout_ms, 0);
|
||||||
Ok(0)
|
Ok([PollFlags::empty(); 2])
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
|
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use rustix::event::{self, EventfdFlags, PollFd, PollFlags, Timespec};
|
||||||
|
use rustix::io::{self as rustix_io, Errno};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct RuntimeWakeSource {
|
pub(crate) struct RuntimeWakeSource {
|
||||||
fd: Arc<OwnedFd>,
|
fd: Arc<OwnedFd>,
|
||||||
@@ -20,33 +23,43 @@ pub(super) enum TerminalReadinessPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn validate_poll_readiness(
|
pub(super) fn validate_poll_readiness(
|
||||||
pollfd: &libc::pollfd,
|
revents: PollFlags,
|
||||||
label: &str,
|
label: &str,
|
||||||
terminal_policy: TerminalReadinessPolicy,
|
terminal_policy: TerminalReadinessPolicy,
|
||||||
) -> io::Result<bool> {
|
) -> io::Result<bool> {
|
||||||
let readable = pollfd.revents & libc::POLLIN != 0;
|
let readable = revents.contains(PollFlags::IN);
|
||||||
if pollfd.revents & libc::POLLNVAL != 0 {
|
if revents.contains(PollFlags::NVAL) {
|
||||||
return Err(io::Error::other(format!(
|
return Err(io::Error::other(format!(
|
||||||
"{label} poll descriptor failed with readiness {:#x}",
|
"{label} poll descriptor failed with readiness {:#x}",
|
||||||
pollfd.revents
|
revents.bits()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let terminal = pollfd.revents & (libc::POLLERR | libc::POLLHUP);
|
let terminal = revents.intersects(PollFlags::ERR | PollFlags::HUP);
|
||||||
if terminal != 0 && !(terminal_policy == TerminalReadinessPolicy::ReadBuffered && readable) {
|
if terminal && !(terminal_policy == TerminalReadinessPolicy::ReadBuffered && readable) {
|
||||||
return Err(io::Error::other(format!(
|
return Err(io::Error::other(format!(
|
||||||
"{label} poll descriptor failed with readiness {:#x}",
|
"{label} poll descriptor failed with readiness {:#x}",
|
||||||
pollfd.revents
|
revents.bits()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if pollfd.revents != 0 && !readable {
|
if !revents.is_empty() && !readable {
|
||||||
return Err(io::Error::other(format!(
|
return Err(io::Error::other(format!(
|
||||||
"{label} poll descriptor returned unexpected readiness {:#x}",
|
"{label} poll descriptor returned unexpected readiness {:#x}",
|
||||||
pollfd.revents
|
revents.bits()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
Ok(readable)
|
Ok(readable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `poll(2)` with a millisecond timeout from [`timeout_to_poll_ms`], where a
|
||||||
|
/// negative value blocks.
|
||||||
|
pub(super) fn poll_for_ms(fds: &mut [PollFd<'_>], timeout_ms: i32) -> io::Result<usize> {
|
||||||
|
let timeout = (timeout_ms >= 0).then(|| Timespec {
|
||||||
|
tv_sec: (timeout_ms / 1_000).into(),
|
||||||
|
tv_nsec: (timeout_ms % 1_000 * 1_000_000).into(),
|
||||||
|
});
|
||||||
|
Ok(event::poll(fds, timeout.as_ref())?)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn poll_with_retry<T>(
|
pub(super) fn poll_with_retry<T>(
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
mut attempt: impl FnMut(i32) -> io::Result<Option<T>>,
|
mut attempt: impl FnMut(i32) -> io::Result<Option<T>>,
|
||||||
@@ -67,15 +80,9 @@ pub(super) fn poll_with_retry<T>(
|
|||||||
|
|
||||||
impl RuntimeWakeSource {
|
impl RuntimeWakeSource {
|
||||||
pub(crate) fn try_new() -> io::Result<Self> {
|
pub(crate) fn try_new() -> io::Result<Self> {
|
||||||
// SAFETY: eventfd returns a new owned descriptor on success. EFD_NONBLOCK
|
// NONBLOCK keeps both producer writes and consumer drains bounded, and
|
||||||
// keeps both producer writes and consumer drains bounded, and EFD_CLOEXEC
|
// CLOEXEC prevents subprocesses from extending the descriptor lifetime.
|
||||||
// prevents subprocesses from extending the runtime descriptor lifetime.
|
let fd = event::eventfd(0, EventfdFlags::NONBLOCK | EventfdFlags::CLOEXEC)?;
|
||||||
let raw_fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
|
|
||||||
if raw_fd < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
// SAFETY: raw_fd was just returned by eventfd and has not been wrapped.
|
|
||||||
let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
|
|
||||||
Ok(Self { fd: Arc::new(fd) })
|
Ok(Self { fd: Arc::new(fd) })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,32 +100,20 @@ impl RuntimeWakeSource {
|
|||||||
/// One eventfd read consumes the entire accumulated counter, so a second
|
/// One eventfd read consumes the entire accumulated counter, so a second
|
||||||
/// successful-path read would only issue a guaranteed-EAGAIN syscall.
|
/// successful-path read would only issue a guaranteed-EAGAIN syscall.
|
||||||
pub(crate) fn drain(&self) -> io::Result<bool> {
|
pub(crate) fn drain(&self) -> io::Result<bool> {
|
||||||
|
let mut value = [0_u8; size_of::<u64>()];
|
||||||
loop {
|
loop {
|
||||||
let mut value = 0_u64;
|
match rustix_io::read(&self.fd, &mut value) {
|
||||||
// SAFETY: value points to a writable u64 and the owned eventfd remains
|
Ok(read) if read == value.len() => return Ok(true),
|
||||||
// valid for the duration of this read.
|
Ok(read) => {
|
||||||
let result = unsafe {
|
return Err(io::Error::new(
|
||||||
libc::read(
|
io::ErrorKind::UnexpectedEof,
|
||||||
self.fd.as_raw_fd(),
|
format!("runtime wake eventfd returned a short read ({read} bytes)"),
|
||||||
(&raw mut value).cast(),
|
));
|
||||||
size_of::<u64>(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if result == size_of::<u64>() as isize {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
if result < 0 {
|
|
||||||
let err = io::Error::last_os_error();
|
|
||||||
match err.kind() {
|
|
||||||
io::ErrorKind::Interrupted => continue,
|
|
||||||
io::ErrorKind::WouldBlock => return Ok(false),
|
|
||||||
_ => return Err(err),
|
|
||||||
}
|
}
|
||||||
|
Err(Errno::INTR) => {}
|
||||||
|
Err(Errno::WOULDBLOCK) => return Ok(false),
|
||||||
|
Err(errno) => return Err(errno.into()),
|
||||||
}
|
}
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::UnexpectedEof,
|
|
||||||
format!("runtime wake eventfd returned a short read ({result} bytes)"),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,24 +121,15 @@ impl RuntimeWakeSource {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn wait_readable(&self, timeout: Option<Duration>) -> io::Result<bool> {
|
pub(crate) fn wait_readable(&self, timeout: Option<Duration>) -> io::Result<bool> {
|
||||||
let readable = poll_with_retry(timeout, |timeout_ms| {
|
let readable = poll_with_retry(timeout, |timeout_ms| {
|
||||||
let mut pollfd = libc::pollfd {
|
let mut pollfds = [PollFd::new(&self.fd, PollFlags::IN)];
|
||||||
fd: self.fd.as_raw_fd(),
|
if poll_for_ms(&mut pollfds, timeout_ms)? == 0 {
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: pollfd is valid and the Arc-owned descriptor remains open
|
|
||||||
// throughout this bounded or producer-woken wait.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut pollfd, 1, timeout_ms) };
|
|
||||||
if ready == 0 {
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
if ready < 0 {
|
let revents = pollfds[0].revents();
|
||||||
return Err(io::Error::last_os_error());
|
if !validate_poll_readiness(revents, "runtime wake", TerminalReadinessPolicy::Reject)? {
|
||||||
}
|
|
||||||
if !validate_poll_readiness(&pollfd, "runtime wake", TerminalReadinessPolicy::Reject)? {
|
|
||||||
return Err(io::Error::other(format!(
|
return Err(io::Error::other(format!(
|
||||||
"runtime wake poll reported readiness without a readable descriptor ({:#x})",
|
"runtime wake poll reported readiness without a readable descriptor ({:#x})",
|
||||||
pollfd.revents
|
revents.bits()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
Ok(Some(()))
|
Ok(Some(()))
|
||||||
@@ -154,6 +140,27 @@ impl RuntimeWakeSource {
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reports whether a wake arrives within `timeout`, without draining it.
|
||||||
|
/// Any other readiness is an error, so a broken descriptor cannot pass for
|
||||||
|
/// silence in a test that expects no wake.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn is_readable_within(&self, timeout: Duration) -> io::Result<bool> {
|
||||||
|
let readable = poll_with_retry(Some(timeout), |timeout_ms| {
|
||||||
|
let mut pollfds = [PollFd::new(&self.fd, PollFlags::IN)];
|
||||||
|
if poll_for_ms(&mut pollfds, timeout_ms)? == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
validate_poll_readiness(
|
||||||
|
pollfds[0].revents(),
|
||||||
|
"runtime wake",
|
||||||
|
TerminalReadinessPolicy::Reject,
|
||||||
|
)
|
||||||
|
.map(Some)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(readable == Some(true))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn timeout_to_poll_ms(timeout: Option<Duration>) -> i32 {
|
pub(super) fn timeout_to_poll_ms(timeout: Option<Duration>) -> i32 {
|
||||||
@@ -169,34 +176,22 @@ pub(super) fn timeout_to_poll_ms(timeout: Option<Duration>) -> i32 {
|
|||||||
|
|
||||||
impl RuntimeWakeHandle {
|
impl RuntimeWakeHandle {
|
||||||
pub(crate) fn wake(&self) -> io::Result<()> {
|
pub(crate) fn wake(&self) -> io::Result<()> {
|
||||||
let value = 1_u64;
|
let value = 1_u64.to_ne_bytes();
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: value points to a readable u64 and the shared eventfd remains
|
match rustix_io::write(&self.fd, &value) {
|
||||||
// valid for the duration of this write.
|
Ok(written) if written == value.len() => return Ok(()),
|
||||||
let result = unsafe {
|
Ok(written) => {
|
||||||
libc::write(
|
return Err(io::Error::new(
|
||||||
self.fd.as_raw_fd(),
|
io::ErrorKind::WriteZero,
|
||||||
(&raw const value).cast(),
|
format!("runtime wake eventfd returned a short write ({written} bytes)"),
|
||||||
size_of::<u64>(),
|
));
|
||||||
)
|
|
||||||
};
|
|
||||||
if result == size_of::<u64>() as isize {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if result < 0 {
|
|
||||||
let err = io::Error::last_os_error();
|
|
||||||
match err.kind() {
|
|
||||||
io::ErrorKind::Interrupted => continue,
|
|
||||||
// A saturated eventfd is already readable, so the wake is
|
|
||||||
// successfully coalesced rather than lost.
|
|
||||||
io::ErrorKind::WouldBlock => return Ok(()),
|
|
||||||
_ => return Err(err),
|
|
||||||
}
|
}
|
||||||
|
Err(Errno::INTR) => {}
|
||||||
|
// A saturated eventfd is already readable, so the wake is
|
||||||
|
// successfully coalesced rather than lost.
|
||||||
|
Err(Errno::WOULDBLOCK) => return Ok(()),
|
||||||
|
Err(errno) => return Err(errno.into()),
|
||||||
}
|
}
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::WriteZero,
|
|
||||||
format!("runtime wake eventfd returned a short write ({result} bytes)"),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,21 +202,13 @@ mod tests {
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{fs, thread};
|
use std::{fs, thread};
|
||||||
|
|
||||||
|
use rustix::pipe;
|
||||||
|
use rustix::process::Pid;
|
||||||
|
use rustix::thread as rustix_thread;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn poll_readable(source: &RuntimeWakeSource, timeout_ms: i32) -> bool {
|
fn wait_until_task_is_blocked_in_poll(tid: Pid) {
|
||||||
let mut pollfd = libc::pollfd {
|
|
||||||
fd: source.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: pollfd is valid for the duration of this call.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut pollfd, 1, timeout_ms) };
|
|
||||||
assert!(ready >= 0, "poll failed: {}", io::Error::last_os_error());
|
|
||||||
ready > 0 && pollfd.revents & libc::POLLIN != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_until_task_is_blocked_in_poll(tid: libc::pid_t) {
|
|
||||||
let stat_path = format!("/proc/self/task/{tid}/stat");
|
let stat_path = format!("/proc/self/task/{tid}/stat");
|
||||||
let deadline = Instant::now() + Duration::from_secs(1);
|
let deadline = Instant::now() + Duration::from_secs(1);
|
||||||
loop {
|
loop {
|
||||||
@@ -248,9 +235,9 @@ mod tests {
|
|||||||
let source = RuntimeWakeSource::try_new().unwrap();
|
let source = RuntimeWakeSource::try_new().unwrap();
|
||||||
source.handle().wake().unwrap();
|
source.handle().wake().unwrap();
|
||||||
|
|
||||||
assert!(poll_readable(&source, 0));
|
assert!(source.is_readable_within(Duration::ZERO).unwrap());
|
||||||
assert!(source.drain().unwrap());
|
assert!(source.drain().unwrap());
|
||||||
assert!(!poll_readable(&source, 0));
|
assert!(!source.is_readable_within(Duration::ZERO).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -259,10 +246,7 @@ mod tests {
|
|||||||
let handle = source.handle();
|
let handle = source.handle();
|
||||||
let (poll_entry_tx, poll_entry_rx) = mpsc::channel();
|
let (poll_entry_tx, poll_entry_rx) = mpsc::channel();
|
||||||
let poller = thread::spawn(move || {
|
let poller = thread::spawn(move || {
|
||||||
// SAFETY: gettid has no preconditions and returns the calling Linux
|
poll_entry_tx.send(rustix_thread::gettid()).unwrap();
|
||||||
// thread's id, used only to observe its scheduler state in this test.
|
|
||||||
let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t };
|
|
||||||
poll_entry_tx.send(tid).unwrap();
|
|
||||||
assert!(source.wait_readable(Some(Duration::from_secs(1))).unwrap());
|
assert!(source.wait_readable(Some(Duration::from_secs(1))).unwrap());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -281,7 +265,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert!(source.drain().unwrap());
|
assert!(source.drain().unwrap());
|
||||||
assert!(!poll_readable(&source, 0));
|
assert!(!source.is_readable_within(Duration::ZERO).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -299,6 +283,15 @@ mod tests {
|
|||||||
assert!(!source.drain().unwrap());
|
assert!(!source.drain().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn readiness_without_a_wake_is_an_error_rather_than_silence() {
|
||||||
|
let (read, write) = pipe::pipe().unwrap();
|
||||||
|
drop(write);
|
||||||
|
let source = RuntimeWakeSource { fd: Arc::new(read) };
|
||||||
|
|
||||||
|
assert!(source.is_readable_within(Duration::ZERO).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wait_readable_times_out_without_a_wake() {
|
fn wait_readable_times_out_without_a_wake() {
|
||||||
let source = RuntimeWakeSource::try_new().unwrap();
|
let source = RuntimeWakeSource::try_new().unwrap();
|
||||||
@@ -307,22 +300,18 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn terminal_readiness_policy_distinguishes_streams_from_wake_descriptors() {
|
fn terminal_readiness_policy_distinguishes_streams_from_wake_descriptors() {
|
||||||
let pollfd = libc::pollfd {
|
let revents = PollFlags::IN | PollFlags::HUP;
|
||||||
fd: 7,
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: libc::POLLIN | libc::POLLHUP,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
validate_poll_readiness(
|
validate_poll_readiness(
|
||||||
&pollfd,
|
revents,
|
||||||
"buffered stream",
|
"buffered stream",
|
||||||
TerminalReadinessPolicy::ReadBuffered,
|
TerminalReadinessPolicy::ReadBuffered,
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
validate_poll_readiness(&pollfd, "runtime wake", TerminalReadinessPolicy::Reject,)
|
validate_poll_readiness(revents, "runtime wake", TerminalReadinessPolicy::Reject)
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,21 +114,13 @@ pub(super) fn setup_signal_handlers(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::os::fd::AsRawFd;
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::backend::wayland::RuntimeWakeSource;
|
use crate::backend::wayland::RuntimeWakeSource;
|
||||||
|
|
||||||
fn wait_for_wake(source: &RuntimeWakeSource) {
|
fn wait_for_wake(source: &RuntimeWakeSource) {
|
||||||
let mut pollfd = libc::pollfd {
|
assert!(source.is_readable_within(Duration::from_secs(1)).unwrap());
|
||||||
fd: source.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: source retains the descriptor throughout this bounded wait.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 1_000) };
|
|
||||||
assert_eq!(ready, 1);
|
|
||||||
assert_ne!(pollfd.revents & libc::POLLIN, 0);
|
|
||||||
source.drain().unwrap();
|
source.drain().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,12 +205,11 @@ fn is_gnome_copied_files_mime(mime_type: &str) -> bool {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io;
|
|
||||||
use std::os::unix;
|
use std::os::unix;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::backend::wayland::clipboard::image::choose_supported_mime;
|
use crate::backend::wayland::clipboard::image::choose_supported_mime;
|
||||||
use crate::test_temp::TempDir;
|
use crate::test_temp::{self, TempDir};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -265,16 +264,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn uri_list_paste_rejects_fifo_without_blocking() {
|
fn uri_list_paste_rejects_fifo_without_blocking() {
|
||||||
use std::ffi::CString;
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
|
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
let fifo_path = temp.path().join("not-an-image");
|
let fifo_path = temp.path().join("not-an-image");
|
||||||
let c_path = CString::new(fifo_path.as_os_str().as_bytes()).unwrap();
|
test_temp::make_fifo(&fifo_path);
|
||||||
// SAFETY: `c_path` is a NUL-terminated string that outlives the call,
|
|
||||||
// and mkfifo only reads it.
|
|
||||||
let result = unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) };
|
|
||||||
assert_eq!(result, 0, "mkfifo failed: {}", io::Error::last_os_error());
|
|
||||||
let uri = file_uri_for_path(&fifo_path);
|
let uri = file_uri_for_path(&fifo_path);
|
||||||
let offered = vec![TEXT_URI_LIST_MIME.to_string()];
|
let offered = vec![TEXT_URI_LIST_MIME.to_string()];
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
//! device or keymap problem can never panic across the libinput FFI edge.
|
//! device or keymap problem can never panic across the libinput FFI edge.
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::{AsRawFd, OwnedFd};
|
use std::os::fd::OwnedFd;
|
||||||
|
|
||||||
|
use rustix::io as rustix_io;
|
||||||
use std::sync::mpsc::{Receiver, TryRecvError, channel};
|
use std::sync::mpsc::{Receiver, TryRecvError, channel};
|
||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -188,15 +190,10 @@ impl InputMonitor {
|
|||||||
/// A reader wedged inside libinput is left to finish on its own rather than
|
/// A reader wedged inside libinput is left to finish on its own rather than
|
||||||
/// blocking overlay shutdown; its descriptors close with the process.
|
/// blocking overlay shutdown; its descriptors close with the process.
|
||||||
fn stop(&mut self) {
|
fn stop(&mut self) {
|
||||||
let byte = [1_u8];
|
if let Err(errno) = rustix_io::write(&self.stop, &[1]) {
|
||||||
// SAFETY: the write end is owned and open for the duration of the call,
|
|
||||||
// and `byte` is a valid one-byte buffer.
|
|
||||||
let written =
|
|
||||||
unsafe { libc::write(self.stop.as_raw_fd(), byte.as_ptr().cast(), byte.len()) };
|
|
||||||
if written < 0 {
|
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Failed to signal the system input reader: {}",
|
"Failed to signal the system input reader: {}",
|
||||||
io::Error::last_os_error()
|
io::Error::from(errno)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let Some(join) = self.join.take() else {
|
let Some(join) = self.join.take() else {
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ use input::event::pointer::{Axis, ButtonState, PointerScrollEvent};
|
|||||||
use input::event::tablet_tool::{TabletToolEvent, TipState};
|
use input::event::tablet_tool::{TabletToolEvent, TipState};
|
||||||
use input::event::{Event, EventTrait, KeyboardEvent, PointerEvent};
|
use input::event::{Event, EventTrait, KeyboardEvent, PointerEvent};
|
||||||
use input::{DeviceCapability, Libinput, LibinputInterface};
|
use input::{DeviceCapability, Libinput, LibinputInterface};
|
||||||
|
use rustix::event::{self, PollFd, PollFlags, Timespec};
|
||||||
|
use rustix::io::Errno;
|
||||||
|
use rustix::pipe::{self, PipeFlags};
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
use std::os::fd::OwnedFd;
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
@@ -28,17 +31,7 @@ use repeat::{KeyboardKeys, RepeatPress, RepeatState};
|
|||||||
|
|
||||||
/// Non-blocking, close-on-exec pipe used only to interrupt the reader's poll.
|
/// Non-blocking, close-on-exec pipe used only to interrupt the reader's poll.
|
||||||
pub(super) fn stop_pipe() -> io::Result<(OwnedFd, OwnedFd)> {
|
pub(super) fn stop_pipe() -> io::Result<(OwnedFd, OwnedFd)> {
|
||||||
let mut fds = [0 as RawFd; 2];
|
pipe::pipe_with(PipeFlags::CLOEXEC).map_err(io::Error::from)
|
||||||
// SAFETY: `fds` is a writable two-element array, the shape pipe2(2) expects.
|
|
||||||
let result = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) };
|
|
||||||
if result < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
// SAFETY: both descriptors were just returned by pipe2 and are unwrapped.
|
|
||||||
let read = unsafe { OwnedFd::from_raw_fd(fds[0]) };
|
|
||||||
// SAFETY: as above for the write end.
|
|
||||||
let write = unsafe { OwnedFd::from_raw_fd(fds[1]) };
|
|
||||||
Ok((read, write))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open evdev nodes for libinput. Unprivileged `open(2)`: it succeeds when the
|
/// Open evdev nodes for libinput. Unprivileged `open(2)`: it succeeds when the
|
||||||
@@ -239,47 +232,33 @@ fn wait_for_input(
|
|||||||
stop_read: &OwnedFd,
|
stop_read: &OwnedFd,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
) -> io::Result<PollOutcome> {
|
) -> io::Result<PollOutcome> {
|
||||||
// A repeat deadline in the past polls with 0 and returns Timeout at once.
|
// A repeat deadline in the past polls with zero and returns Timeout at once.
|
||||||
let timeout_ms = timeout.map_or(-1, |timeout| {
|
let timeout = timeout
|
||||||
timeout.as_millis().min(i32::MAX as u128) as i32
|
.map(Timespec::try_from)
|
||||||
});
|
.transpose()
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
loop {
|
loop {
|
||||||
let mut fds = [
|
let mut fds = [
|
||||||
libc::pollfd {
|
PollFd::new(libinput, PollFlags::IN),
|
||||||
fd: libinput.as_raw_fd(),
|
PollFd::new(stop_read, PollFlags::IN),
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
},
|
|
||||||
libc::pollfd {
|
|
||||||
fd: stop_read.as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
// SAFETY: `fds` is a valid two-element array and both descriptors are
|
match event::poll(&mut fds, timeout.as_ref()) {
|
||||||
// owned (or borrowed from an owner) for the duration of the call. A
|
Ok(0) => return Ok(PollOutcome::Timeout),
|
||||||
// negative timeout blocks until one becomes ready.
|
Ok(_) => {}
|
||||||
let ready = unsafe { libc::poll(fds.as_mut_ptr(), 2, timeout_ms) };
|
Err(Errno::INTR) => continue,
|
||||||
if ready < 0 {
|
Err(errno) => return Err(errno.into()),
|
||||||
let err = io::Error::last_os_error();
|
|
||||||
if err.kind() == io::ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Err(err);
|
|
||||||
}
|
}
|
||||||
if ready == 0 {
|
if !fds[1].revents().is_empty() {
|
||||||
return Ok(PollOutcome::Timeout);
|
|
||||||
}
|
|
||||||
if fds[1].revents != 0 {
|
|
||||||
return Ok(PollOutcome::Stop);
|
return Ok(PollOutcome::Stop);
|
||||||
}
|
}
|
||||||
if fds[0].revents & (libc::POLLERR | libc::POLLNVAL | libc::POLLHUP) != 0 {
|
let revents = fds[0].revents();
|
||||||
|
if revents.intersects(PollFlags::ERR | PollFlags::NVAL | PollFlags::HUP) {
|
||||||
return Err(io::Error::other(format!(
|
return Err(io::Error::other(format!(
|
||||||
"libinput poll descriptor failed with readiness {:#x}",
|
"libinput poll descriptor failed with readiness {:#x}",
|
||||||
fds[0].revents
|
revents.bits()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if fds[0].revents & libc::POLLIN != 0 {
|
if revents.contains(PollFlags::IN) {
|
||||||
return Ok(PollOutcome::Ready);
|
return Ok(PollOutcome::Ready);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
//! silently pick overlay mode and `system` can warn with actionable guidance
|
//! silently pick overlay mode and `system` can warn with actionable guidance
|
||||||
//! before anything is spawned.
|
//! before anything is spawned.
|
||||||
|
|
||||||
use std::ffi::CString;
|
use rustix::fs::{self as rustix_fs, Access};
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use rustix::io::Errno;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::env_vars;
|
use crate::env_vars;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::{env, fs, io};
|
use std::{env, fs};
|
||||||
|
|
||||||
const DEV_INPUT_DIR: &str = "/dev/input";
|
const DEV_INPUT_DIR: &str = "/dev/input";
|
||||||
/// Seat to follow when the session does not name one. libinput's udev backend
|
/// Seat to follow when the session does not name one. libinput's udev backend
|
||||||
@@ -211,18 +211,11 @@ fn classify_seat_nodes(seat: &str) -> EventNodeAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn node_path_access(path: &Path) -> NodePathAccess {
|
fn node_path_access(path: &Path) -> NodePathAccess {
|
||||||
let Ok(c_path) = CString::new(path.as_os_str().as_bytes()) else {
|
match rustix_fs::access(path, Access::READ_OK) {
|
||||||
return NodePathAccess::Unknown;
|
Ok(()) => NodePathAccess::Readable,
|
||||||
};
|
Err(Errno::ACCESS | Errno::PERM) => NodePathAccess::PermissionDenied,
|
||||||
// SAFETY: `c_path` is a valid NUL-terminated C string that outlives the
|
Err(Errno::NOENT | Errno::NOTDIR | Errno::NODEV) => NodePathAccess::Absent,
|
||||||
// call, and `access(2)` only reads it.
|
Err(_) => NodePathAccess::Unknown,
|
||||||
if unsafe { libc::access(c_path.as_ptr(), libc::R_OK) } == 0 {
|
|
||||||
return NodePathAccess::Readable;
|
|
||||||
}
|
|
||||||
match io::Error::last_os_error().raw_os_error() {
|
|
||||||
Some(libc::EACCES | libc::EPERM) => NodePathAccess::PermissionDenied,
|
|
||||||
Some(libc::ENOENT | libc::ENOTDIR | libc::ENODEV) => NodePathAccess::Absent,
|
|
||||||
_ => NodePathAccess::Unknown,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -217,7 +217,6 @@ impl<T> Drop for PortalTaskExitGuard<T> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::future;
|
use std::future;
|
||||||
use std::os::fd::AsRawFd;
|
|
||||||
|
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
|
||||||
@@ -225,13 +224,7 @@ mod tests {
|
|||||||
use crate::backend::wayland::RuntimeWakeSource;
|
use crate::backend::wayland::RuntimeWakeSource;
|
||||||
|
|
||||||
fn wait_for_wake(wake: &RuntimeWakeSource) {
|
fn wait_for_wake(wake: &RuntimeWakeSource) {
|
||||||
let mut pollfd = libc::pollfd {
|
assert!(wake.is_readable_within(Duration::from_secs(1)).unwrap());
|
||||||
fd: wake.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: the descriptor and pollfd remain valid during the bounded wait.
|
|
||||||
assert_eq!(unsafe { libc::poll(&raw mut pollfd, 1, 1_000) }, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
@@ -264,14 +257,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
drop(task);
|
drop(task);
|
||||||
task::yield_now().await;
|
task::yield_now().await;
|
||||||
let mut pollfd = libc::pollfd {
|
assert!(!wake.is_readable_within(Duration::ZERO).unwrap());
|
||||||
fd: wake.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: the descriptor and pollfd are valid for this non-blocking poll.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 0) };
|
|
||||||
assert_eq!(ready, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -379,12 +379,14 @@ impl<T> Drop for RuntimeOperationExitGuard<T> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::os::fd::AsRawFd;
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{fs, thread};
|
use std::{fs, thread};
|
||||||
|
|
||||||
|
use rustix::process::Pid;
|
||||||
|
use rustix::thread as rustix_thread;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::backend::wayland::RuntimeWakeSource;
|
use crate::backend::wayland::RuntimeWakeSource;
|
||||||
|
|
||||||
@@ -396,17 +398,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_wake(wake: &RuntimeWakeSource) {
|
fn wait_for_wake(wake: &RuntimeWakeSource) {
|
||||||
let mut pollfd = libc::pollfd {
|
assert!(wake.is_readable_within(Duration::from_secs(1)).unwrap());
|
||||||
fd: wake.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: pollfd and the runtime wake descriptor remain valid for the bounded wait.
|
|
||||||
assert_eq!(unsafe { libc::poll(&raw mut pollfd, 1, 1_000) }, 1);
|
|
||||||
assert_ne!(pollfd.revents & libc::POLLIN, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_until_thread_is_blocked_in_poll(tid: libc::pid_t) {
|
fn wait_until_thread_is_blocked_in_poll(tid: Pid) {
|
||||||
let stat_path = format!("/proc/self/task/{tid}/stat");
|
let stat_path = format!("/proc/self/task/{tid}/stat");
|
||||||
let deadline = Instant::now() + Duration::from_secs(1);
|
let deadline = Instant::now() + Duration::from_secs(1);
|
||||||
loop {
|
loop {
|
||||||
@@ -516,9 +511,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let (tid_tx, tid_rx) = mpsc::channel();
|
let (tid_tx, tid_rx) = mpsc::channel();
|
||||||
let poller = thread::spawn(move || {
|
let poller = thread::spawn(move || {
|
||||||
// SAFETY: gettid has no preconditions and is used only to observe this test thread.
|
tid_tx.send(rustix_thread::gettid()).unwrap();
|
||||||
let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t };
|
|
||||||
tid_tx.send(tid).unwrap();
|
|
||||||
wait_for_wake(&wake);
|
wait_for_wake(&wake);
|
||||||
});
|
});
|
||||||
let tid = tid_rx.recv().unwrap();
|
let tid = tid_rx.recv().unwrap();
|
||||||
@@ -592,13 +585,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
assert!(!controller.is_active());
|
assert!(!controller.is_active());
|
||||||
let mut pollfd = libc::pollfd {
|
assert!(!wake.is_readable_within(Duration::ZERO).unwrap());
|
||||||
fd: wake.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: pollfd and the runtime wake descriptor are valid for this non-blocking poll.
|
|
||||||
assert_eq!(unsafe { libc::poll(&raw mut pollfd, 1, 0) }, 0);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
controller
|
controller
|
||||||
.try_submit(42, "test-after-spawn-failure", || 2)
|
.try_submit(42, "test-after-spawn-failure", || 2)
|
||||||
|
|||||||
@@ -470,9 +470,7 @@ fn assert_send_static<T: Send + 'static>() {}
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io::ErrorKind;
|
use std::fs;
|
||||||
use std::os::fd::AsRawFd;
|
|
||||||
use std::{fs, io};
|
|
||||||
|
|
||||||
use crate::test_temp;
|
use crate::test_temp;
|
||||||
|
|
||||||
@@ -503,27 +501,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_runtime_wake(wake: &RuntimeWakeSource) {
|
fn wait_for_runtime_wake(wake: &RuntimeWakeSource) {
|
||||||
let mut pollfd = libc::pollfd {
|
assert!(
|
||||||
fd: wake.poll_fd().as_raw_fd(),
|
wake.is_readable_within(Duration::from_secs(1)).unwrap(),
|
||||||
events: libc::POLLIN,
|
"persistence worker did not wake runtime within one second"
|
||||||
revents: 0,
|
);
|
||||||
};
|
|
||||||
loop {
|
|
||||||
// SAFETY: pollfd and the runtime-owned descriptor remain valid for
|
|
||||||
// this bounded test wait.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 1_000) };
|
|
||||||
if ready > 0 {
|
|
||||||
assert_ne!(pollfd.revents & libc::POLLIN, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ready == 0 {
|
|
||||||
panic!("persistence worker did not wake runtime within one second");
|
|
||||||
}
|
|
||||||
let err = io::Error::last_os_error();
|
|
||||||
if err.kind() != ErrorKind::Interrupted {
|
|
||||||
panic!("runtime wake poll failed: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use rustix::process as rustix_process;
|
||||||
|
|
||||||
use crate::config::ConfigDocument;
|
use crate::config::ConfigDocument;
|
||||||
use crate::{config, test_temp};
|
use crate::{config, test_temp};
|
||||||
|
|
||||||
@@ -387,8 +389,7 @@ fn a_read_only_config_fails_the_write_and_leaves_the_file_alone() {
|
|||||||
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir");
|
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir");
|
||||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file");
|
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file");
|
||||||
|
|
||||||
// SAFETY: geteuid has no preconditions and cannot fail.
|
if rustix_process::geteuid().is_root() {
|
||||||
if unsafe { libc::geteuid() } == 0 {
|
|
||||||
// Root ignores these modes; the scenario cannot be staged.
|
// Root ignores these modes; the scenario cannot be staged.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ impl WaylandState {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use rustix::process as rustix_process;
|
||||||
|
|
||||||
use crate::config::ConfigDocument;
|
use crate::config::ConfigDocument;
|
||||||
use crate::input::Tool;
|
use crate::input::Tool;
|
||||||
use crate::{config, test_temp};
|
use crate::{config, test_temp};
|
||||||
@@ -367,8 +369,7 @@ redo = [\"Ctrl+Alt+Shift+Q\"]
|
|||||||
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir");
|
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir");
|
||||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file");
|
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file");
|
||||||
|
|
||||||
// SAFETY: geteuid has no preconditions and cannot fail.
|
if rustix_process::geteuid().is_root() {
|
||||||
if unsafe { libc::geteuid() } == 0 {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assert!(result.is_err(), "a read-only config must fail the write");
|
assert!(result.is_err(), "a read-only config must fail the write");
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ impl WaylandState {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use rustix::process as rustix_process;
|
||||||
|
|
||||||
use crate::config::{ColorSpec, ConfigDocument};
|
use crate::config::{ColorSpec, ConfigDocument};
|
||||||
use crate::{config, test_temp};
|
use crate::{config, test_temp};
|
||||||
|
|
||||||
@@ -343,8 +345,7 @@ color = \"#33F54F\"
|
|||||||
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir");
|
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir");
|
||||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file");
|
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file");
|
||||||
|
|
||||||
// SAFETY: geteuid has no preconditions and cannot fail.
|
if rustix_process::geteuid().is_root() {
|
||||||
if unsafe { libc::geteuid() } == 0 {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assert!(result.is_err(), "a read-only config must fail the write");
|
assert!(result.is_err(), "a read-only config must fail the write");
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::os::fd::AsRawFd;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, Mutex, mpsc};
|
use std::sync::{Arc, Mutex, mpsc};
|
||||||
@@ -163,23 +162,19 @@ async fn completion_wakes_an_already_blocked_runtime_poll_after_publication() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
started.notified().await;
|
started.notified().await;
|
||||||
|
|
||||||
let wake_fd = runtime_wake.poll_fd().as_raw_fd();
|
|
||||||
let (polling_tx, polling_rx) = mpsc::sync_channel(0);
|
let (polling_tx, polling_rx) = mpsc::sync_channel(0);
|
||||||
let poller = thread::spawn(move || {
|
let poller = thread::spawn(move || {
|
||||||
let mut pollfd = libc::pollfd {
|
|
||||||
fd: wake_fd,
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
polling_tx.send(()).unwrap();
|
polling_tx.send(()).unwrap();
|
||||||
// SAFETY: the runtime wake source outlives this bounded poll and owns
|
assert!(
|
||||||
// the descriptor for the entire test.
|
runtime_wake
|
||||||
assert_eq!(unsafe { libc::poll(&raw mut pollfd, 1, 1_000) }, 1);
|
.is_readable_within(Duration::from_secs(1))
|
||||||
assert_ne!(pollfd.revents & libc::POLLIN, 0);
|
.unwrap()
|
||||||
|
);
|
||||||
|
runtime_wake
|
||||||
});
|
});
|
||||||
polling_rx.recv().unwrap();
|
polling_rx.recv().unwrap();
|
||||||
release.notify_one();
|
release.notify_one();
|
||||||
poller.join().unwrap();
|
let runtime_wake = poller.join().unwrap();
|
||||||
runtime_wake.drain().unwrap();
|
runtime_wake.drain().unwrap();
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
|
|||||||
+94
-142
@@ -2,6 +2,8 @@ use std::env;
|
|||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use lexopt::{Arg, Parser};
|
||||||
|
|
||||||
use crate::domain::Language;
|
use crate::domain::Language;
|
||||||
use crate::tray_action::TrayAction;
|
use crate::tray_action::TrayAction;
|
||||||
use crate::{build_info, i18n, runtime_capabilities};
|
use crate::{build_info, i18n, runtime_capabilities};
|
||||||
@@ -105,96 +107,70 @@ impl Cli {
|
|||||||
I: IntoIterator<Item = T>,
|
I: IntoIterator<Item = T>,
|
||||||
T: Into<OsString>,
|
T: Into<OsString>,
|
||||||
{
|
{
|
||||||
let mut args = args
|
let mut parser = Parser::from_iter(args.into_iter().map(Into::into));
|
||||||
.into_iter()
|
|
||||||
.map(|arg| {
|
|
||||||
arg.into()
|
|
||||||
.into_string()
|
|
||||||
.map_err(|_| "arguments must be valid UTF-8".to_string())
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
if args.is_empty() {
|
|
||||||
args.push("wayscriber".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cli = Cli::default();
|
let mut cli = Cli::default();
|
||||||
let mut index = 1;
|
|
||||||
while index < args.len() {
|
while let Some(arg) = parser.next().map_err(|error| parse_error(&error))? {
|
||||||
let arg = &args[index];
|
match arg {
|
||||||
match arg.as_str() {
|
Arg::Short('h') => return Ok(CliOutcome::Help),
|
||||||
"-h" | "--help" => return Ok(CliOutcome::Help),
|
Arg::Short('V') => return Ok(CliOutcome::Version),
|
||||||
"-V" | "--version" => return Ok(CliOutcome::Version),
|
Arg::Long("help") => {
|
||||||
"-d" | "--daemon" => cli.daemon = true,
|
reject_attached_value(&mut parser, "--help")?;
|
||||||
"--daemon-toggle" => cli.daemon_toggle = true,
|
return Ok(CliOutcome::Help);
|
||||||
"--daemon-action" => {
|
|
||||||
index += 1;
|
|
||||||
cli.daemon_action = Some(value_after(&args, index, "--daemon-action")?);
|
|
||||||
}
|
}
|
||||||
"--light-toggle" => cli.light_toggle = true,
|
Arg::Long("version") => {
|
||||||
"--light-draw-toggle" => cli.light_draw_toggle = true,
|
reject_attached_value(&mut parser, "--version")?;
|
||||||
"--light-draw-on" => cli.light_draw_on = true,
|
return Ok(CliOutcome::Version);
|
||||||
"--light-draw-off" => cli.light_draw_off = true,
|
|
||||||
"-a" | "--active" => cli.active = true,
|
|
||||||
"-m" | "--mode" => {
|
|
||||||
index += 1;
|
|
||||||
cli.mode = Some(value_after(&args, index, "--mode")?);
|
|
||||||
}
|
}
|
||||||
"--no-tray" => cli.no_tray = true,
|
Arg::Short('d') | Arg::Long("daemon") => cli.daemon = true,
|
||||||
"--freeze-on-show" => cli.freeze_on_show = true,
|
Arg::Long("daemon-toggle") => cli.daemon_toggle = true,
|
||||||
"--clear-session" => cli.clear_session = true,
|
Arg::Long("daemon-action") => {
|
||||||
"--clear-tool-state" => cli.clear_tool_state = true,
|
cli.daemon_action = Some(string_value(&mut parser, "--daemon-action")?);
|
||||||
"--session-info" => cli.session_info = true,
|
|
||||||
"--rename-session" => {
|
|
||||||
index += 1;
|
|
||||||
cli.rename_session = Some(value_after(&args, index, "--rename-session")?);
|
|
||||||
}
|
}
|
||||||
"--session-file" => {
|
Arg::Long("light-toggle") => cli.light_toggle = true,
|
||||||
index += 1;
|
Arg::Long("light-draw-toggle") => cli.light_draw_toggle = true,
|
||||||
|
Arg::Long("light-draw-on") => cli.light_draw_on = true,
|
||||||
|
Arg::Long("light-draw-off") => cli.light_draw_off = true,
|
||||||
|
Arg::Short('a') | Arg::Long("active") => cli.active = true,
|
||||||
|
Arg::Short('m') => cli.mode = Some(string_value(&mut parser, "-m")?),
|
||||||
|
Arg::Long("mode") => cli.mode = Some(string_value(&mut parser, "--mode")?),
|
||||||
|
Arg::Long("no-tray") => cli.no_tray = true,
|
||||||
|
Arg::Long("freeze-on-show") => cli.freeze_on_show = true,
|
||||||
|
Arg::Long("clear-session") => cli.clear_session = true,
|
||||||
|
Arg::Long("clear-tool-state") => cli.clear_tool_state = true,
|
||||||
|
Arg::Long("session-info") => cli.session_info = true,
|
||||||
|
Arg::Long("rename-session") => {
|
||||||
|
cli.rename_session = Some(string_value(&mut parser, "--rename-session")?);
|
||||||
|
}
|
||||||
|
Arg::Long("session-file") => {
|
||||||
cli.session_file =
|
cli.session_file =
|
||||||
Some(PathBuf::from(value_after(&args, index, "--session-file")?));
|
Some(PathBuf::from(option_value(&mut parser, "--session-file")?));
|
||||||
}
|
}
|
||||||
"--freeze" => cli.freeze = true,
|
Arg::Long("freeze") => cli.freeze = true,
|
||||||
"--exit-after-capture" => cli.exit_after_capture = true,
|
Arg::Long("exit-after-capture") => cli.exit_after_capture = true,
|
||||||
"--no-exit-after-capture" => cli.no_exit_after_capture = true,
|
Arg::Long("no-exit-after-capture") => cli.no_exit_after_capture = true,
|
||||||
"--resume-session" => cli.resume_session = true,
|
Arg::Long("resume-session") => cli.resume_session = true,
|
||||||
"--no-resume-session" => cli.no_resume_session = true,
|
Arg::Long("no-resume-session") => cli.no_resume_session = true,
|
||||||
"--about" => cli.about = true,
|
Arg::Long("about") => cli.about = true,
|
||||||
"--check-update" => cli.check_update = true,
|
Arg::Long("check-update") => cli.check_update = true,
|
||||||
"--language" => {
|
Arg::Long("language") => {
|
||||||
index += 1;
|
cli.language = Some(parse_language(&string_value(&mut parser, "--language")?)?);
|
||||||
cli.language = Some(parse_language(&value_after(&args, index, "--language")?)?);
|
|
||||||
}
|
}
|
||||||
"--list-languages" => cli.list_languages = true,
|
Arg::Long("list-languages") => cli.list_languages = true,
|
||||||
runtime_capabilities::RUNTIME_CAPABILITIES_FLAG => {
|
Arg::Long(name)
|
||||||
|
if runtime_capabilities::RUNTIME_CAPABILITIES_FLAG.strip_prefix("--")
|
||||||
|
== Some(name) =>
|
||||||
|
{
|
||||||
cli.runtime_capabilities = true;
|
cli.runtime_capabilities = true;
|
||||||
}
|
}
|
||||||
_ if arg.starts_with("--daemon-action=") => {
|
Arg::Long(name) => {
|
||||||
cli.daemon_action = Some(value_from_equals(arg, "--daemon-action")?);
|
let option = format!("--{name}");
|
||||||
|
reject_attached_value(&mut parser, &option)?;
|
||||||
|
return Err(unknown_argument(&option));
|
||||||
}
|
}
|
||||||
_ if arg.starts_with("--mode=") => {
|
Arg::Short(flag) => return Err(format!("unknown short option '-{flag}'")),
|
||||||
cli.mode = Some(value_from_equals(arg, "--mode")?);
|
Arg::Value(value) => return Err(unknown_argument(&value.display().to_string())),
|
||||||
}
|
|
||||||
_ if arg.starts_with("--session-file=") => {
|
|
||||||
cli.session_file =
|
|
||||||
Some(PathBuf::from(value_from_equals(arg, "--session-file")?));
|
|
||||||
}
|
|
||||||
_ if arg.starts_with("--rename-session=") => {
|
|
||||||
cli.rename_session = Some(value_from_equals(arg, "--rename-session")?);
|
|
||||||
}
|
|
||||||
_ if arg.starts_with("--language=") => {
|
|
||||||
cli.language = Some(parse_language(&value_from_equals(arg, "--language")?)?);
|
|
||||||
}
|
|
||||||
_ if is_short_option_cluster(arg) => {
|
|
||||||
match parse_short_option_cluster(&args, index, &mut cli)? {
|
|
||||||
ShortOptionOutcome::Continue(next_index) => index = next_index,
|
|
||||||
ShortOptionOutcome::Help => return Ok(CliOutcome::Help),
|
|
||||||
ShortOptionOutcome::Version => return Ok(CliOutcome::Version),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => return Err(format!("unknown argument '{arg}'")),
|
|
||||||
}
|
}
|
||||||
index += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.validate()?;
|
cli.validate()?;
|
||||||
@@ -525,77 +501,53 @@ fn parse_language(value: &str) -> Result<Language, String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_short_option_cluster(arg: &str) -> bool {
|
/// Reads an option's value, either attached (`--mode=x`, `-mx`, `-m=x`) or as
|
||||||
arg.starts_with('-') && !arg.starts_with("--") && arg.len() > 2
|
/// the next argument. A next argument starting with `-` is the next option, so
|
||||||
|
/// a value that starts with `-` has to be attached.
|
||||||
|
fn option_value(parser: &mut Parser, name: &str) -> Result<OsString, String> {
|
||||||
|
let requires_value = || format!("{name} requires a value");
|
||||||
|
|
||||||
|
if let Some(value) = parser.optional_value() {
|
||||||
|
return if value.is_empty() {
|
||||||
|
Err(requires_value())
|
||||||
|
} else {
|
||||||
|
Ok(value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = parser.value().map_err(|_| requires_value())?;
|
||||||
|
if value.as_encoded_bytes().starts_with(b"-") {
|
||||||
|
return Err(requires_value());
|
||||||
|
}
|
||||||
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ShortOptionOutcome {
|
fn string_value(parser: &mut Parser, name: &str) -> Result<String, String> {
|
||||||
Continue(usize),
|
option_value(parser, name)?
|
||||||
Help,
|
.into_string()
|
||||||
Version,
|
.map_err(|_| "arguments must be valid UTF-8".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_short_option_cluster(
|
/// Rejects `--flag=value` for an option that is about to stop parsing, which
|
||||||
args: &[String],
|
/// would otherwise skip the check `Parser::next` makes on its following call.
|
||||||
index: usize,
|
fn reject_attached_value(parser: &mut Parser, name: &str) -> Result<(), String> {
|
||||||
cli: &mut Cli,
|
match parser.optional_value() {
|
||||||
) -> Result<ShortOptionOutcome, String> {
|
Some(value) => Err(unknown_argument(&format!("{name}={}", value.display()))),
|
||||||
let arg = &args[index];
|
None => Ok(()),
|
||||||
for (offset, flag) in arg[1..].char_indices() {
|
}
|
||||||
match flag {
|
}
|
||||||
'h' => return Ok(ShortOptionOutcome::Help),
|
|
||||||
'V' => return Ok(ShortOptionOutcome::Version),
|
fn parse_error(error: &lexopt::Error) -> String {
|
||||||
'd' => cli.daemon = true,
|
match error {
|
||||||
'a' => cli.active = true,
|
lexopt::Error::UnexpectedValue { option, value } => {
|
||||||
'm' => {
|
unknown_argument(&format!("{option}={}", value.display()))
|
||||||
let value_start = 1 + offset + flag.len_utf8();
|
|
||||||
let value = if value_start < arg.len() {
|
|
||||||
attached_short_value(arg, value_start, "-m")?
|
|
||||||
} else {
|
|
||||||
value_after(args, index + 1, "-m")?
|
|
||||||
};
|
|
||||||
cli.mode = Some(value);
|
|
||||||
return Ok(ShortOptionOutcome::Continue(if value_start < arg.len() {
|
|
||||||
index
|
|
||||||
} else {
|
|
||||||
index + 1
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
_ => return Err(format!("unknown short option '-{flag}'")),
|
|
||||||
}
|
}
|
||||||
|
other => other.to_string(),
|
||||||
}
|
}
|
||||||
Ok(ShortOptionOutcome::Continue(index))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value_after(args: &[String], index: usize, name: &str) -> Result<String, String> {
|
fn unknown_argument(arg: &str) -> String {
|
||||||
let value = args
|
format!("unknown argument '{arg}'")
|
||||||
.get(index)
|
|
||||||
.ok_or_else(|| format!("{name} requires a value"))?;
|
|
||||||
if value.starts_with('-') {
|
|
||||||
return Err(format!("{name} requires a value"));
|
|
||||||
}
|
|
||||||
Ok(value.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attached_short_value(arg: &str, value_start: usize, name: &str) -> Result<String, String> {
|
|
||||||
let value = arg[value_start..]
|
|
||||||
.strip_prefix('=')
|
|
||||||
.unwrap_or_else(|| &arg[value_start..]);
|
|
||||||
if value.is_empty() {
|
|
||||||
return Err(format!("{name} requires a value"));
|
|
||||||
}
|
|
||||||
Ok(value.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn value_from_equals(arg: &str, name: &str) -> Result<String, String> {
|
|
||||||
let value = arg
|
|
||||||
.split_once('=')
|
|
||||||
.map(|(_, value)| value)
|
|
||||||
.unwrap_or_default();
|
|
||||||
if value.is_empty() {
|
|
||||||
return Err(format!("{name} requires a value"));
|
|
||||||
}
|
|
||||||
Ok(value.to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn conflict(left: &str, right: &str) -> String {
|
fn conflict(left: &str, right: &str) -> String {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::ffi::OsString;
|
||||||
|
use std::os::unix::ffi::OsStringExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::{Cli, CliOutcome};
|
use super::{Cli, CliOutcome};
|
||||||
@@ -601,3 +603,104 @@ fn list_languages_is_a_print_and_exit_command() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_value_starting_with_a_dash_must_be_attached() {
|
||||||
|
for args in [
|
||||||
|
&["wayscriber", "--mode", "-x"][..],
|
||||||
|
&["wayscriber", "-m", "-x"],
|
||||||
|
&["wayscriber", "--mode", "--active"],
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
Cli::try_parse_from(args.iter().copied())
|
||||||
|
.unwrap_err()
|
||||||
|
.ends_with("requires a value"),
|
||||||
|
"{args:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_cli(["wayscriber", "--mode=-x"]).mode.as_deref(),
|
||||||
|
Some("-x")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_cli(["wayscriber", "-m=-x"]).mode.as_deref(),
|
||||||
|
Some("-x")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_cli(["wayscriber", "-m-x"]).mode.as_deref(),
|
||||||
|
Some("-x")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_empty_attached_value_is_missing_but_a_separate_one_is_not() {
|
||||||
|
for (args, name) in [
|
||||||
|
(&["wayscriber", "--mode="][..], "--mode"),
|
||||||
|
(&["wayscriber", "-m="], "-m"),
|
||||||
|
(&["wayscriber", "--language="], "--language"),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
Cli::try_parse_from(args.iter().copied()).unwrap_err(),
|
||||||
|
format!("{name} requires a value")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_cli(["wayscriber", "--mode", ""]).mode.as_deref(),
|
||||||
|
Some("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_and_misused_options_are_rejected() {
|
||||||
|
for (args, expected) in [
|
||||||
|
(&["wayscriber", "--nope"][..], "unknown argument '--nope'"),
|
||||||
|
(&["wayscriber", "--nope=1"], "unknown argument '--nope=1'"),
|
||||||
|
(
|
||||||
|
&["wayscriber", "--active=1"],
|
||||||
|
"unknown argument '--active=1'",
|
||||||
|
),
|
||||||
|
(&["wayscriber", "--help=1"], "unknown argument '--help=1'"),
|
||||||
|
(
|
||||||
|
&["wayscriber", "--version=1"],
|
||||||
|
"unknown argument '--version=1'",
|
||||||
|
),
|
||||||
|
(&["wayscriber", "stray"], "unknown argument 'stray'"),
|
||||||
|
(&["wayscriber", "-"], "unknown argument '-'"),
|
||||||
|
(&["wayscriber", "--", "stray"], "unknown argument 'stray'"),
|
||||||
|
(&["wayscriber", "-x"], "unknown short option '-x'"),
|
||||||
|
(&["wayscriber", "-dx"], "unknown short option '-x'"),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
Cli::try_parse_from(args.iter().copied()).unwrap_err(),
|
||||||
|
expected,
|
||||||
|
"{args:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_session_path_keeps_its_bytes_but_other_values_must_be_utf8() {
|
||||||
|
let raw_path = OsString::from_vec(b"/tmp/s\xff.wayscriber-session".to_vec());
|
||||||
|
|
||||||
|
let outcome = Cli::try_parse_from([
|
||||||
|
OsString::from("wayscriber"),
|
||||||
|
OsString::from("--active"),
|
||||||
|
OsString::from("--session-file"),
|
||||||
|
raw_path.clone(),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
let CliOutcome::Run(cli) = outcome else {
|
||||||
|
panic!("expected CLI run outcome, got {outcome:?}");
|
||||||
|
};
|
||||||
|
assert_eq!(cli.session_file, Some(PathBuf::from(raw_path)));
|
||||||
|
|
||||||
|
let error = Cli::try_parse_from([
|
||||||
|
OsString::from("wayscriber"),
|
||||||
|
OsString::from("--mode"),
|
||||||
|
OsString::from_vec(b"\xff".to_vec()),
|
||||||
|
])
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(error, "arguments must be valid UTF-8");
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ use crate::paths::{daemon_lock_file, daemon_pid_file};
|
|||||||
use crate::session::try_lock_exclusive;
|
use crate::session::try_lock_exclusive;
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use rustix::process::{self as rustix_process, Pid, Signal};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
@@ -136,16 +138,13 @@ pub(super) fn signal_daemon_pid(pid: u32) -> Result<()> {
|
|||||||
return Err(anyhow!("invalid daemon pid {pid}"));
|
return Err(anyhow!("invalid daemon pid {pid}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: `pid` has been checked to be a positive Unix process id and
|
let target = Pid::from_raw(pid).ok_or_else(|| anyhow!("invalid daemon pid {pid}"))?;
|
||||||
// `SIGUSR1` is a valid signal constant.
|
rustix_process::kill_process(target, Signal::USR1).map_err(|errno| {
|
||||||
if unsafe { libc::kill(pid, libc::SIGUSR1) } != 0 {
|
anyhow!(
|
||||||
return Err(anyhow!(
|
"failed to signal wayscriber daemon {pid}: {}",
|
||||||
"failed to signal wayscriber daemon {}: {}",
|
io::Error::from(errno)
|
||||||
pid,
|
)
|
||||||
io::Error::last_os_error()
|
})
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
|
|||||||
+29
-52
@@ -3,10 +3,12 @@ use std::{io, mem, process, thread};
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
|
use rustix::event::{self, PollFd, PollFlags};
|
||||||
|
use rustix::io::Errno;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
use std::os::fd::BorrowedFd;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||||
@@ -922,48 +924,31 @@ fn wait_for_daemon_lifecycle(
|
|||||||
deadline_source: Option<&BootDeadlineSource>,
|
deadline_source: Option<&BootDeadlineSource>,
|
||||||
overlay_exit_fd: Option<BorrowedFd<'_>>,
|
overlay_exit_fd: Option<BorrowedFd<'_>>,
|
||||||
) -> Result<DaemonLifecycleReadiness> {
|
) -> Result<DaemonLifecycleReadiness> {
|
||||||
let mut pollfds = vec![libc::pollfd {
|
let mut pollfds = vec![PollFd::from_borrowed_fd(
|
||||||
fd: daemon_wake.poll_fd().as_raw_fd(),
|
daemon_wake.poll_fd(),
|
||||||
events: libc::POLLIN,
|
PollFlags::IN,
|
||||||
revents: 0,
|
)];
|
||||||
}];
|
|
||||||
if let Some(watcher) = command_watcher {
|
if let Some(watcher) = command_watcher {
|
||||||
pollfds.push(libc::pollfd {
|
pollfds.push(PollFd::from_borrowed_fd(watcher.poll_fd(), PollFlags::IN));
|
||||||
fd: watcher.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
let command_index = command_watcher.map(|_| 1);
|
let command_index = command_watcher.map(|_| 1);
|
||||||
let deadline_index = deadline_source.map(|source| {
|
let deadline_index = deadline_source.map(|source| {
|
||||||
let index = pollfds.len();
|
let index = pollfds.len();
|
||||||
pollfds.push(libc::pollfd {
|
pollfds.push(PollFd::from_borrowed_fd(source.poll_fd(), PollFlags::IN));
|
||||||
fd: source.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
});
|
|
||||||
index
|
index
|
||||||
});
|
});
|
||||||
let child_index = overlay_exit_fd.map(|fd| {
|
let child_index = overlay_exit_fd.map(|fd| {
|
||||||
let index = pollfds.len();
|
let index = pollfds.len();
|
||||||
pollfds.push(libc::pollfd {
|
pollfds.push(PollFd::from_borrowed_fd(fd, PollFlags::IN));
|
||||||
fd: fd.as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
});
|
|
||||||
index
|
index
|
||||||
});
|
});
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: the descriptor remains owned by `daemon_wake` throughout poll.
|
let ready = match event::poll(&mut pollfds, None) {
|
||||||
let ready = unsafe {
|
Ok(ready) => ready,
|
||||||
libc::poll(
|
Err(Errno::INTR) => continue,
|
||||||
pollfds.as_mut_ptr(),
|
Err(errno) => {
|
||||||
pollfds
|
return Err(io::Error::from(errno)).context("daemon lifecycle poll failed");
|
||||||
.len()
|
}
|
||||||
.try_into()
|
|
||||||
.expect("poll descriptor count fits"),
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
if ready == 0 {
|
if ready == 0 {
|
||||||
return Ok(DaemonLifecycleReadiness {
|
return Ok(DaemonLifecycleReadiness {
|
||||||
@@ -971,36 +956,28 @@ fn wait_for_daemon_lifecycle(
|
|||||||
deadline: false,
|
deadline: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ready < 0 {
|
|
||||||
let err = io::Error::last_os_error();
|
|
||||||
if err.kind() == ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Err(err).context("daemon lifecycle poll failed");
|
|
||||||
}
|
|
||||||
for pollfd in &pollfds {
|
for pollfd in &pollfds {
|
||||||
let terminal = pollfd.revents & (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL);
|
let revents = pollfd.revents();
|
||||||
if terminal != 0 {
|
if revents.intersects(PollFlags::ERR | PollFlags::HUP | PollFlags::NVAL) {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"daemon lifecycle descriptor returned terminal readiness {:#x}",
|
"daemon lifecycle descriptor returned terminal readiness {:#x}",
|
||||||
pollfd.revents
|
revents.bits()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let daemon_ready = pollfds[0].revents & libc::POLLIN != 0;
|
let is_readable = |index: Option<usize>| {
|
||||||
let command_ready = command_index
|
index
|
||||||
.and_then(|index| pollfds.get(index))
|
.and_then(|index| pollfds.get(index))
|
||||||
.is_some_and(|pollfd| pollfd.revents & libc::POLLIN != 0);
|
.is_some_and(|pollfd| pollfd.revents().contains(PollFlags::IN))
|
||||||
let deadline_ready = deadline_index
|
};
|
||||||
.and_then(|index| pollfds.get(index))
|
let daemon_ready = is_readable(Some(0));
|
||||||
.is_some_and(|pollfd| pollfd.revents & libc::POLLIN != 0);
|
let command_ready = is_readable(command_index);
|
||||||
let child_ready = child_index
|
let deadline_ready = is_readable(deadline_index);
|
||||||
.and_then(|index| pollfds.get(index))
|
let child_ready = is_readable(child_index);
|
||||||
.is_some_and(|pollfd| pollfd.revents & libc::POLLIN != 0);
|
|
||||||
if !daemon_ready && !command_ready && !deadline_ready && !child_ready {
|
if !daemon_ready && !command_ready && !deadline_ready && !child_ready {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"daemon wake descriptor returned invalid readiness {:#x}",
|
"daemon wake descriptor returned invalid readiness {:#x}",
|
||||||
pollfds[0].revents
|
pollfds[0].revents().bits()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if daemon_ready {
|
if daemon_ready {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
use std::os::fd::{AsFd, BorrowedFd};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||||
@@ -108,7 +108,7 @@ impl OverlayLifecycle {
|
|||||||
&candidate.program,
|
&candidate.program,
|
||||||
&launch.arguments,
|
&launch.arguments,
|
||||||
launch.environment,
|
launch.environment,
|
||||||
daemon_watchdog.as_raw_fd(),
|
daemon_watchdog.as_fd(),
|
||||||
)?;
|
)?;
|
||||||
self.child.start(child)?;
|
self.child.start(child)?;
|
||||||
self.child
|
self.child
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fs::{self, File, OpenOptions};
|
use std::fs::{self, File, OpenOptions};
|
||||||
use std::io::{self, ErrorKind};
|
use std::io::{self, ErrorKind};
|
||||||
use std::os::fd::AsRawFd;
|
|
||||||
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -11,6 +10,7 @@ use std::sync::LazyLock;
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use rustix::fs::{self as rustix_fs, FlockOperation};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -224,10 +224,13 @@ fn try_open_journal_lock(root: &Path, nonblocking: bool) -> Result<Option<File>>
|
|||||||
if !file.metadata()?.is_file() {
|
if !file.metadata()?.is_file() {
|
||||||
bail!("action journal lock is not a regular file");
|
bail!("action journal lock is not a regular file");
|
||||||
}
|
}
|
||||||
let operation = libc::LOCK_EX | if nonblocking { libc::LOCK_NB } else { 0 };
|
let operation = if nonblocking {
|
||||||
// SAFETY: file owns the descriptor; flock retains no pointer.
|
FlockOperation::NonBlockingLockExclusive
|
||||||
if unsafe { libc::flock(file.as_raw_fd(), operation) } != 0 {
|
} else {
|
||||||
let error = io::Error::last_os_error();
|
FlockOperation::LockExclusive
|
||||||
|
};
|
||||||
|
if let Err(errno) = rustix_fs::flock(&file, operation) {
|
||||||
|
let error = io::Error::from(errno);
|
||||||
if nonblocking && error.kind() == ErrorKind::WouldBlock {
|
if nonblocking && error.kind() == ErrorKind::WouldBlock {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -237,12 +240,9 @@ fn try_open_journal_lock(root: &Path, nonblocking: bool) -> Result<Option<File>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn unlock(file: &File) -> Result<()> {
|
fn unlock(file: &File) -> Result<()> {
|
||||||
// SAFETY: file owns the descriptor; flock retains no pointer.
|
rustix_fs::flock(file, FlockOperation::Unlock)
|
||||||
if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) } == 0 {
|
.map_err(io::Error::from)
|
||||||
Ok(())
|
.context("failed to unlock action journal")
|
||||||
} else {
|
|
||||||
Err(io::Error::last_os_error()).context("failed to unlock action journal")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_record<T: Serialize>(path: &Path, value: &T) -> Result<()> {
|
fn write_record<T: Serialize>(path: &Path, value: &T) -> Result<()> {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use std::{env, thread};
|
use std::{env, thread};
|
||||||
|
|
||||||
|
use rustix::fs::{self as rustix_fs, FlockOperation};
|
||||||
|
|
||||||
use crate::{test_env, test_temp};
|
use crate::{test_env, test_temp};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -179,21 +181,11 @@ fn event_loop_claim_and_finish_defer_instead_of_waiting_for_locks() {
|
|||||||
.write(true)
|
.write(true)
|
||||||
.open(decision_path)
|
.open(decision_path)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
rustix_fs::flock(&held_decision, FlockOperation::LockExclusive).unwrap();
|
||||||
// SAFETY: `held_decision` keeps the descriptor open for the call,
|
|
||||||
// and flock retains no pointer.
|
|
||||||
unsafe { libc::flock(held_decision.as_raw_fd(), libc::LOCK_EX) },
|
|
||||||
0
|
|
||||||
);
|
|
||||||
let ActionFinishOutcome::Deferred(action) = action.try_finish(true, None).unwrap() else {
|
let ActionFinishOutcome::Deferred(action) = action.try_finish(true, None).unwrap() else {
|
||||||
panic!("contended command finish should defer");
|
panic!("contended command finish should defer");
|
||||||
};
|
};
|
||||||
assert_eq!(
|
rustix_fs::flock(&held_decision, FlockOperation::Unlock).unwrap();
|
||||||
// SAFETY: `held_decision` keeps the descriptor open for the call,
|
|
||||||
// and flock retains no pointer.
|
|
||||||
unsafe { libc::flock(held_decision.as_raw_fd(), libc::LOCK_UN) },
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
action.try_finish(true, None).unwrap(),
|
action.try_finish(true, None).unwrap(),
|
||||||
ActionFinishOutcome::Complete
|
ActionFinishOutcome::Complete
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
|
use std::os::fd::{AsFd, BorrowedFd, FromRawFd, OwnedFd};
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::thread::Builder;
|
use std::thread::Builder;
|
||||||
@@ -7,6 +7,8 @@ use std::time::Duration;
|
|||||||
use std::{env, fs, io, process, thread};
|
use std::{env, fs, io, process, thread};
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use rustix::event::{self, PollFd, PollFlags};
|
||||||
|
use rustix::io::{self as rustix_io, Errno, FdFlags};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy};
|
use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy};
|
||||||
@@ -535,11 +537,9 @@ pub(crate) fn start_daemon_watchdog_from_environment() -> Result<()> {
|
|||||||
// inherited descriptor. The environment marker is removed immediately so
|
// inherited descriptor. The environment marker is removed immediately so
|
||||||
// no later exec can reinterpret the descriptor number.
|
// no later exec can reinterpret the descriptor number.
|
||||||
let descriptor = unsafe { OwnedFd::from_raw_fd(raw) };
|
let descriptor = unsafe { OwnedFd::from_raw_fd(raw) };
|
||||||
// SAFETY: descriptor is owned and F_SETFD changes only exec inheritance.
|
rustix_io::fcntl_setfd(&descriptor, FdFlags::CLOEXEC)
|
||||||
if unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) } != 0 {
|
.map_err(io::Error::from)
|
||||||
return Err(io::Error::last_os_error())
|
.context("failed to protect daemon watchdog descriptor")?;
|
||||||
.context("failed to protect daemon watchdog descriptor");
|
|
||||||
}
|
|
||||||
super::linux::validate_pidfd(descriptor.as_fd())
|
super::linux::validate_pidfd(descriptor.as_fd())
|
||||||
.context("inherited daemon watchdog is not a live pidfd")?;
|
.context("inherited daemon watchdog is not a live pidfd")?;
|
||||||
// SAFETY: application startup has not launched runtime worker threads yet;
|
// SAFETY: application startup has not launched runtime worker threads yet;
|
||||||
@@ -550,27 +550,12 @@ pub(crate) fn start_daemon_watchdog_from_environment() -> Result<()> {
|
|||||||
Builder::new()
|
Builder::new()
|
||||||
.name("wayscriber-daemon-watchdog".into())
|
.name("wayscriber-daemon-watchdog".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
let mut pollfd = libc::pollfd {
|
let mut pollfds = [PollFd::new(&descriptor, PollFlags::IN)];
|
||||||
fd: descriptor.as_raw_fd(),
|
while event::poll(&mut pollfds, None) == Err(Errno::INTR) {}
|
||||||
events: libc::POLLIN,
|
// SAFETY: readiness means the owning daemon is gone, and any other
|
||||||
revents: 0,
|
// result is an unusable parent-death channel. Neither can safely
|
||||||
};
|
// preserve overlay ownership, so this is an irrevocable fail-stop.
|
||||||
loop {
|
unsafe { super::linux::fail_stop(70) }
|
||||||
// SAFETY: pollfd points to one initialized entry and the
|
|
||||||
// descriptor remains owned by this thread.
|
|
||||||
let result = unsafe { libc::poll(&raw mut pollfd, 1, -1) };
|
|
||||||
if result > 0 {
|
|
||||||
// SAFETY: loss of the owning daemon is an irrevocable
|
|
||||||
// fail-stop condition for its internal overlay child.
|
|
||||||
unsafe { super::linux::fail_stop(70) }
|
|
||||||
}
|
|
||||||
if result < 0 && io::Error::last_os_error().kind() == ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// SAFETY: an unusable parent-death channel cannot safely
|
|
||||||
// preserve overlay ownership.
|
|
||||||
unsafe { super::linux::fail_stop(70) }
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.context("failed to start daemon watchdog")?;
|
.context("failed to start daemon watchdog")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::io::ErrorKind;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
|
use rustix::fs::FlockOperation;
|
||||||
|
|
||||||
use super::super::BootClock;
|
use super::super::BootClock;
|
||||||
use super::super::action::PreparedAction;
|
use super::super::action::PreparedAction;
|
||||||
@@ -37,14 +38,14 @@ fn claim_command_action_inner(
|
|||||||
let path = control_path(&command_root(), command_identity);
|
let path = control_path(&command_root(), command_identity);
|
||||||
let decision = open_lock(&path.join("decision.lock"), false)?;
|
let decision = open_lock(&path.join("decision.lock"), false)?;
|
||||||
if nonblocking {
|
if nonblocking {
|
||||||
match flock(&decision, libc::LOCK_EX | libc::LOCK_NB) {
|
match flock(&decision, FlockOperation::NonBlockingLockExclusive) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(None),
|
Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(None),
|
||||||
Err(error) => return Err(error).context("failed to try command-action decision lock"),
|
Err(error) => return Err(error).context("failed to try command-action decision lock"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
||||||
lock_until(&decision, libc::LOCK_EX, deadline)?;
|
lock_until(&decision, FlockOperation::LockExclusive, deadline)?;
|
||||||
}
|
}
|
||||||
let mut control = read_control(&path)?;
|
let mut control = read_control(&path)?;
|
||||||
let exact = |status: &ActionStatus| match status {
|
let exact = |status: &ActionStatus| match status {
|
||||||
@@ -193,14 +194,14 @@ fn finish_command_action_inner(
|
|||||||
let path = control_path(&command_root(), command_identity);
|
let path = control_path(&command_root(), command_identity);
|
||||||
let decision = open_lock(&path.join("decision.lock"), false)?;
|
let decision = open_lock(&path.join("decision.lock"), false)?;
|
||||||
if nonblocking {
|
if nonblocking {
|
||||||
match flock(&decision, libc::LOCK_EX | libc::LOCK_NB) {
|
match flock(&decision, FlockOperation::NonBlockingLockExclusive) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(false),
|
Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(false),
|
||||||
Err(error) => return Err(error).context("failed to try command-action decision lock"),
|
Err(error) => return Err(error).context("failed to try command-action decision lock"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
||||||
lock_until(&decision, libc::LOCK_EX, deadline)?;
|
lock_until(&decision, FlockOperation::LockExclusive, deadline)?;
|
||||||
}
|
}
|
||||||
let mut control = read_control(&path)?;
|
let mut control = read_control(&path)?;
|
||||||
let applied = result == CommandActionResult::Applied;
|
let applied = result == CommandActionResult::Applied;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Result, anyhow, bail};
|
use anyhow::{Result, anyhow, bail};
|
||||||
|
use rustix::fs::FlockOperation;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use super::super::wire::CommandControl;
|
use super::super::wire::CommandControl;
|
||||||
@@ -61,7 +62,8 @@ impl ClaimedCommand {
|
|||||||
journal.prepare_command(&expected.identity, &expected.target_daemon_token, action)?;
|
journal.prepare_command(&expected.identity, &expected.target_daemon_token, action)?;
|
||||||
let deadline =
|
let deadline =
|
||||||
BootDeadline::from_nanos(expected.submission_clock.authorization_deadline_boottime_ns);
|
BootDeadline::from_nanos(expected.submission_clock.authorization_deadline_boottime_ns);
|
||||||
if let Err(error) = lock_until(&self.decision_lock, libc::LOCK_EX, deadline) {
|
if let Err(error) = lock_until(&self.decision_lock, FlockOperation::LockExclusive, deadline)
|
||||||
|
{
|
||||||
let _ = journal.abandon(&prepared, "command decision lock was not reacquired");
|
let _ = journal.abandon(&prepared, "command decision lock was not reacquired");
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
@@ -71,7 +73,11 @@ impl ClaimedCommand {
|
|||||||
let reason = "command was canceled during action preparation";
|
let reason = "command was canceled during action preparation";
|
||||||
journal.abandon(&prepared, reason)?;
|
journal.abandon(&prepared, reason)?;
|
||||||
let recovery_deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
let recovery_deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
||||||
lock_until(&self.decision_lock, libc::LOCK_EX, recovery_deadline)?;
|
lock_until(
|
||||||
|
&self.decision_lock,
|
||||||
|
FlockOperation::LockExclusive,
|
||||||
|
recovery_deadline,
|
||||||
|
)?;
|
||||||
let mut latest = read_control(&self.control_path)?;
|
let mut latest = read_control(&self.control_path)?;
|
||||||
if latest != current
|
if latest != current
|
||||||
|| !matches!(
|
|| !matches!(
|
||||||
@@ -221,6 +227,6 @@ impl ClaimedCommand {
|
|||||||
|
|
||||||
impl Drop for ClaimedCommand {
|
impl Drop for ClaimedCommand {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = flock(&self.decision_lock, libc::LOCK_UN);
|
let _ = flock(&self.decision_lock, FlockOperation::Unlock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ use std::time::Duration;
|
|||||||
use std::{fs, process, thread};
|
use std::{fs, process, thread};
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
|
use rustix::fs::FlockOperation;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
@@ -62,8 +63,16 @@ impl ClientCommand {
|
|||||||
create_private_directory(&stage)?;
|
create_private_directory(&stage)?;
|
||||||
let decision_lock = open_lock(&stage.join("decision.lock"), true)?;
|
let decision_lock = open_lock(&stage.join("decision.lock"), true)?;
|
||||||
let caller_lease = open_lock(&stage.join("caller.lease"), true)?;
|
let caller_lease = open_lock(&stage.join("caller.lease"), true)?;
|
||||||
lock_until(&caller_lease, libc::LOCK_SH, authorization_deadline)?;
|
lock_until(
|
||||||
lock_until(&decision_lock, libc::LOCK_EX, authorization_deadline)?;
|
&caller_lease,
|
||||||
|
FlockOperation::LockShared,
|
||||||
|
authorization_deadline,
|
||||||
|
)?;
|
||||||
|
lock_until(
|
||||||
|
&decision_lock,
|
||||||
|
FlockOperation::LockExclusive,
|
||||||
|
authorization_deadline,
|
||||||
|
)?;
|
||||||
|
|
||||||
let mut control = CommandControl {
|
let mut control = CommandControl {
|
||||||
protocol_version: DAEMON_COMMAND_PROTOCOL_VERSION,
|
protocol_version: DAEMON_COMMAND_PROTOCOL_VERSION,
|
||||||
@@ -154,7 +163,7 @@ impl ClientCommand {
|
|||||||
let slice = now
|
let slice = now
|
||||||
.checked_add(Duration::from_millis(20))?
|
.checked_add(Duration::from_millis(20))?
|
||||||
.min(self.response_deadline);
|
.min(self.response_deadline);
|
||||||
if !try_lock_until(&self.decision_lock, libc::LOCK_EX, slice)? {
|
if !try_lock_until(&self.decision_lock, FlockOperation::LockExclusive, slice)? {
|
||||||
if slice < self.response_deadline {
|
if slice < self.response_deadline {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -201,7 +210,11 @@ impl ClientCommand {
|
|||||||
let _ = unlock(&self.caller_lease);
|
let _ = unlock(&self.caller_lease);
|
||||||
return Ok(TerminalCommandResult::AdmittedIndeterminate(reason));
|
return Ok(TerminalCommandResult::AdmittedIndeterminate(reason));
|
||||||
}
|
}
|
||||||
lock_until(&self.decision_lock, libc::LOCK_EX, self.response_deadline)?;
|
lock_until(
|
||||||
|
&self.decision_lock,
|
||||||
|
FlockOperation::LockExclusive,
|
||||||
|
self.response_deadline,
|
||||||
|
)?;
|
||||||
let mut control = read_control(&self.control_path)?;
|
let mut control = read_control(&self.control_path)?;
|
||||||
if cancel_open(&mut control)? {
|
if cancel_open(&mut control)? {
|
||||||
write_control(&self.control_path, &control)?;
|
write_control(&self.control_path, &control)?;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::fs::{self, File, OpenOptions};
|
use std::fs::{self, File, OpenOptions};
|
||||||
use std::io::{self, ErrorKind};
|
use std::io::{self, ErrorKind};
|
||||||
use std::os::fd::AsRawFd;
|
|
||||||
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use rustix::fs::{self as rustix_fs, FlockOperation};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
use crate::durable_io::AtomicWriteOptions;
|
use crate::durable_io::AtomicWriteOptions;
|
||||||
@@ -279,22 +279,25 @@ pub(super) fn open_lock(path: &Path, create_new: bool) -> Result<File> {
|
|||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn flock(file: &File, operation: libc::c_int) -> io::Result<()> {
|
pub(super) fn flock(file: &File, operation: FlockOperation) -> io::Result<()> {
|
||||||
// SAFETY: file owns a valid descriptor and flock does not retain it.
|
rustix_fs::flock(file, operation).map_err(io::Error::from)
|
||||||
if unsafe { libc::flock(file.as_raw_fd(), operation) } == 0 {
|
}
|
||||||
Ok(())
|
|
||||||
} else {
|
fn nonblocking(operation: FlockOperation) -> FlockOperation {
|
||||||
Err(io::Error::last_os_error())
|
match operation {
|
||||||
|
FlockOperation::LockShared => FlockOperation::NonBlockingLockShared,
|
||||||
|
FlockOperation::LockExclusive => FlockOperation::NonBlockingLockExclusive,
|
||||||
|
other => other,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn unlock(file: &File) -> Result<()> {
|
pub(super) fn unlock(file: &File) -> Result<()> {
|
||||||
flock(file, libc::LOCK_UN).context("failed to unlock protocol descriptor")
|
flock(file, FlockOperation::Unlock).context("failed to unlock protocol descriptor")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn lock_until(
|
pub(super) fn lock_until(
|
||||||
file: &File,
|
file: &File,
|
||||||
operation: libc::c_int,
|
operation: FlockOperation,
|
||||||
deadline: BootDeadline,
|
deadline: BootDeadline,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if try_lock_until(file, operation, deadline)? {
|
if try_lock_until(file, operation, deadline)? {
|
||||||
@@ -306,11 +309,11 @@ pub(super) fn lock_until(
|
|||||||
|
|
||||||
pub(super) fn try_lock_until(
|
pub(super) fn try_lock_until(
|
||||||
file: &File,
|
file: &File,
|
||||||
operation: libc::c_int,
|
operation: FlockOperation,
|
||||||
deadline: BootDeadline,
|
deadline: BootDeadline,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
loop {
|
loop {
|
||||||
match flock(file, operation | libc::LOCK_NB) {
|
match flock(file, nonblocking(operation)) {
|
||||||
Ok(()) => return Ok(true),
|
Ok(()) => return Ok(true),
|
||||||
Err(error) if error.kind() == ErrorKind::WouldBlock => {
|
Err(error) if error.kind() == ErrorKind::WouldBlock => {
|
||||||
if BootClock::now()? >= deadline {
|
if BootClock::now()? >= deadline {
|
||||||
@@ -341,7 +344,7 @@ pub(super) fn try_admission_lock(root: &Path, deadline: BootDeadline) -> Result<
|
|||||||
}
|
}
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
if try_lock_until(&file, libc::LOCK_EX, deadline)? {
|
if try_lock_until(&file, FlockOperation::LockExclusive, deadline)? {
|
||||||
Ok(Some(file))
|
Ok(Some(file))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
|
use rustix::fs::FlockOperation;
|
||||||
|
|
||||||
use super::super::wire::{
|
use super::super::wire::{
|
||||||
ActionStatus, CallerDisposition, ChildReportStatus, CommandDecision, CommandResponse,
|
ActionStatus, CallerDisposition, ChildReportStatus, CommandDecision, CommandResponse,
|
||||||
@@ -191,7 +192,11 @@ impl CommandOwner {
|
|||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
let retry_deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
let retry_deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
||||||
if !try_lock_until(&decision_lock, libc::LOCK_EX, retry_deadline)? {
|
if !try_lock_until(
|
||||||
|
&decision_lock,
|
||||||
|
FlockOperation::LockExclusive,
|
||||||
|
retry_deadline,
|
||||||
|
)? {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let Ok(mut control) = read_control(&path) else {
|
let Ok(mut control) = read_control(&path) else {
|
||||||
@@ -316,7 +321,7 @@ impl CommandOwner {
|
|||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
||||||
lock_until(&decision, libc::LOCK_EX, deadline)?;
|
lock_until(&decision, FlockOperation::LockExclusive, deadline)?;
|
||||||
let mut control = read_control(&path)?;
|
let mut control = read_control(&path)?;
|
||||||
if control.identity == reference.command_identity
|
if control.identity == reference.command_identity
|
||||||
&& control.queue_order == reference.queue_order
|
&& control.queue_order == reference.queue_order
|
||||||
@@ -341,13 +346,13 @@ impl CommandOwner {
|
|||||||
observed: &super::super::wire::CommandControl,
|
observed: &super::super::wire::CommandControl,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let caller_lease = open_lock(&path.join("caller.lease"), false)?;
|
let caller_lease = open_lock(&path.join("caller.lease"), false)?;
|
||||||
match flock(&caller_lease, libc::LOCK_EX | libc::LOCK_NB) {
|
match flock(&caller_lease, FlockOperation::NonBlockingLockExclusive) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(()),
|
Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(()),
|
||||||
Err(error) => return Err(error).context("failed to inspect unqueued caller lease"),
|
Err(error) => return Err(error).context("failed to inspect unqueued caller lease"),
|
||||||
}
|
}
|
||||||
let decision = open_lock(&path.join("decision.lock"), false)?;
|
let decision = open_lock(&path.join("decision.lock"), false)?;
|
||||||
match flock(&decision, libc::LOCK_EX | libc::LOCK_NB) {
|
match flock(&decision, FlockOperation::NonBlockingLockExclusive) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(error) if error.kind() == ErrorKind::WouldBlock => {
|
Err(error) if error.kind() == ErrorKind::WouldBlock => {
|
||||||
unlock(&caller_lease)?;
|
unlock(&caller_lease)?;
|
||||||
@@ -390,7 +395,7 @@ impl CommandOwner {
|
|||||||
}
|
}
|
||||||
let decision_lock = open_lock(&path.join("decision.lock"), false)?;
|
let decision_lock = open_lock(&path.join("decision.lock"), false)?;
|
||||||
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
||||||
if !try_lock_until(&decision_lock, libc::LOCK_EX, deadline)? {
|
if !try_lock_until(&decision_lock, FlockOperation::LockExclusive, deadline)? {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let mut control = read_control(&path)?;
|
let mut control = read_control(&path)?;
|
||||||
@@ -440,11 +445,11 @@ impl CommandOwner {
|
|||||||
for entry in read_dir_bounded(&controls_dir(&self.root), MAX_DIRECTORY_ENTRIES_PER_SCAN)? {
|
for entry in read_dir_bounded(&controls_dir(&self.root), MAX_DIRECTORY_ENTRIES_PER_SCAN)? {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let lease = open_lock(&path.join("caller.lease"), false)?;
|
let lease = open_lock(&path.join("caller.lease"), false)?;
|
||||||
if flock(&lease, libc::LOCK_EX | libc::LOCK_NB).is_err() {
|
if flock(&lease, FlockOperation::NonBlockingLockExclusive).is_err() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let decision = open_lock(&path.join("decision.lock"), false)?;
|
let decision = open_lock(&path.join("decision.lock"), false)?;
|
||||||
if flock(&decision, libc::LOCK_EX | libc::LOCK_NB).is_err() {
|
if flock(&decision, FlockOperation::NonBlockingLockExclusive).is_err() {
|
||||||
unlock(&lease)?;
|
unlock(&lease)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::path::Path;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Result, anyhow, bail};
|
use anyhow::{Result, anyhow, bail};
|
||||||
|
use rustix::fs::FlockOperation;
|
||||||
|
|
||||||
use super::super::BootClock;
|
use super::super::BootClock;
|
||||||
use super::super::wire::{
|
use super::super::wire::{
|
||||||
@@ -32,7 +33,7 @@ pub(super) fn recover_previous_generation(root: &Path, daemon_token: &str) -> Re
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?;
|
||||||
lock_until(&decision_lock, libc::LOCK_EX, deadline)?;
|
lock_until(&decision_lock, FlockOperation::LockExclusive, deadline)?;
|
||||||
let Ok(mut control) = read_control(&path) else {
|
let Ok(mut control) = read_control(&path) else {
|
||||||
unlock(&decision_lock)?;
|
unlock(&decision_lock)?;
|
||||||
quarantine_entry(root, &path, QuarantineKind::Control)?;
|
quarantine_entry(root, &path, QuarantineKind::Control)?;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::path::Path;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use rustix::fs::FlockOperation;
|
||||||
|
|
||||||
use super::super::{BootClock, BootIdentity};
|
use super::super::{BootClock, BootIdentity};
|
||||||
use super::MAX_COMMAND_STAGING_DIRECTORIES;
|
use super::MAX_COMMAND_STAGING_DIRECTORIES;
|
||||||
@@ -24,7 +25,7 @@ pub(super) fn recover_staging(root: &Path) -> Result<()> {
|
|||||||
let _ = fs::remove_dir_all(path);
|
let _ = fs::remove_dir_all(path);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if flock(&lease, libc::LOCK_EX | libc::LOCK_NB).is_ok() {
|
if flock(&lease, FlockOperation::NonBlockingLockExclusive).is_ok() {
|
||||||
fs::remove_dir_all(path)?;
|
fs::remove_dir_all(path)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use rustix::fs::FlockOperation;
|
||||||
|
|
||||||
use crate::{test_env, test_temp};
|
use crate::{test_env, test_temp};
|
||||||
|
|
||||||
use super::super::BootClock;
|
use super::super::BootClock;
|
||||||
@@ -208,7 +210,7 @@ fn contended_published_command_claim_is_deferred_and_retried() {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
flock(&client.decision_lock, libc::LOCK_EX).unwrap();
|
flock(&client.decision_lock, FlockOperation::LockExclusive).unwrap();
|
||||||
|
|
||||||
assert!(owner.claim_next().unwrap().is_none());
|
assert!(owner.claim_next().unwrap().is_none());
|
||||||
let now = BootClock::now().unwrap();
|
let now = BootClock::now().unwrap();
|
||||||
@@ -252,7 +254,7 @@ fn contended_ref_less_recovery_is_deferred_and_retried() {
|
|||||||
&control.identity,
|
&control.identity,
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
flock(&client.decision_lock, libc::LOCK_EX).unwrap();
|
flock(&client.decision_lock, FlockOperation::LockExclusive).unwrap();
|
||||||
|
|
||||||
assert!(owner.claim_next().unwrap().is_none());
|
assert!(owner.claim_next().unwrap().is_none());
|
||||||
unlock(&client.decision_lock).unwrap();
|
unlock(&client.decision_lock).unwrap();
|
||||||
@@ -287,7 +289,7 @@ fn terminal_collection_defers_while_admission_is_busy() {
|
|||||||
drop(client);
|
drop(client);
|
||||||
|
|
||||||
let admission = open_lock(&owner.root.join("admission.lock"), false).unwrap();
|
let admission = open_lock(&owner.root.join("admission.lock"), false).unwrap();
|
||||||
flock(&admission, libc::LOCK_EX).unwrap();
|
flock(&admission, FlockOperation::LockExclusive).unwrap();
|
||||||
assert_eq!(owner.collect_terminal().unwrap(), 0);
|
assert_eq!(owner.collect_terminal().unwrap(), 0);
|
||||||
let now = BootClock::now().unwrap();
|
let now = BootClock::now().unwrap();
|
||||||
let retry = owner
|
let retry = owner
|
||||||
@@ -715,20 +717,20 @@ fn lock_deadline_is_reported_without_parsing_error_text() {
|
|||||||
let path = root.join("structured-timeout.lock");
|
let path = root.join("structured-timeout.lock");
|
||||||
let held = open_lock(&path, true).unwrap();
|
let held = open_lock(&path, true).unwrap();
|
||||||
let contender = open_lock(&path, false).unwrap();
|
let contender = open_lock(&path, false).unwrap();
|
||||||
flock(&held, libc::LOCK_EX).unwrap();
|
flock(&held, FlockOperation::LockExclusive).unwrap();
|
||||||
|
|
||||||
let deadline = BootClock::now()
|
let deadline = BootClock::now()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.checked_add(Duration::from_millis(5))
|
.checked_add(Duration::from_millis(5))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!try_lock_until(&contender, libc::LOCK_EX, deadline).unwrap());
|
assert!(!try_lock_until(&contender, FlockOperation::LockExclusive, deadline).unwrap());
|
||||||
|
|
||||||
unlock(&held).unwrap();
|
unlock(&held).unwrap();
|
||||||
let deadline = BootClock::now()
|
let deadline = BootClock::now()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.checked_add(Duration::from_millis(5))
|
.checked_add(Duration::from_millis(5))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(try_lock_until(&contender, libc::LOCK_EX, deadline).unwrap());
|
assert!(try_lock_until(&contender, FlockOperation::LockExclusive, deadline).unwrap());
|
||||||
unlock(&contender).unwrap();
|
unlock(&contender).unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-184
@@ -1,12 +1,21 @@
|
|||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::fs::{File, Metadata, OpenOptions, Permissions};
|
use std::fs::{File, Metadata, OpenOptions, Permissions};
|
||||||
use std::io::{self, Read};
|
use std::io::{self, Read};
|
||||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
|
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
|
||||||
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{fmt, fs, process, ptr};
|
use std::{fmt, fs, process, ptr};
|
||||||
|
|
||||||
|
use rustix::event::{self, PollFd, PollFlags, Timespec};
|
||||||
|
use rustix::fs::inotify::{self, CreateFlags, WatchFlags};
|
||||||
|
use rustix::io::{self as rustix_io, Errno};
|
||||||
|
use rustix::process::{self as rustix_process, Pid, PidfdFlags};
|
||||||
|
use rustix::time::{
|
||||||
|
self as rustix_time, DynamicClockId, Itimerspec, TimerfdClockId, TimerfdFlags,
|
||||||
|
TimerfdTimerFlags,
|
||||||
|
};
|
||||||
|
|
||||||
const BOOT_ID_PATH: &str = "/proc/sys/kernel/random/boot_id";
|
const BOOT_ID_PATH: &str = "/proc/sys/kernel/random/boot_id";
|
||||||
const TIME_NAMESPACE_PATH: &str = "/proc/self/ns/time";
|
const TIME_NAMESPACE_PATH: &str = "/proc/self/ns/time";
|
||||||
const PID_NAMESPACE_PATH: &str = "/proc/self/ns/pid";
|
const PID_NAMESPACE_PATH: &str = "/proc/self/ns/pid";
|
||||||
@@ -63,33 +72,6 @@ pub(crate) struct CommandQueueWatcher {
|
|||||||
buffered_offset: usize,
|
buffered_offset: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_random(bytes: &mut [u8]) -> io::Result<()> {
|
|
||||||
let mut filled = 0;
|
|
||||||
while filled < bytes.len() {
|
|
||||||
// SAFETY: the remaining slice is writable for its full length. getrandom
|
|
||||||
// writes at most that length and does not retain the pointer.
|
|
||||||
let result = unsafe {
|
|
||||||
libc::getrandom(bytes[filled..].as_mut_ptr().cast(), bytes.len() - filled, 0)
|
|
||||||
};
|
|
||||||
if result > 0 {
|
|
||||||
filled += result as usize;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if result == 0 {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::UnexpectedEof,
|
|
||||||
"getrandom returned zero bytes",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let error = io::Error::last_os_error();
|
|
||||||
if error.kind() == io::ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lowercase_hex(bytes: &[u8], output: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn lowercase_hex(bytes: &[u8], output: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
for byte in bytes {
|
for byte in bytes {
|
||||||
write!(output, "{byte:02x}")?;
|
write!(output, "{byte:02x}")?;
|
||||||
@@ -100,7 +82,7 @@ fn lowercase_hex(bytes: &[u8], output: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|||||||
impl ProtocolId {
|
impl ProtocolId {
|
||||||
pub(crate) fn generate() -> io::Result<Self> {
|
pub(crate) fn generate() -> io::Result<Self> {
|
||||||
let mut bytes = [0; 16];
|
let mut bytes = [0; 16];
|
||||||
fill_random(&mut bytes)?;
|
getrandom::fill(&mut bytes)?;
|
||||||
Ok(Self(bytes))
|
Ok(Self(bytes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +102,7 @@ impl fmt::Display for ProtocolId {
|
|||||||
impl ProtocolToken {
|
impl ProtocolToken {
|
||||||
pub(crate) fn generate() -> io::Result<Self> {
|
pub(crate) fn generate() -> io::Result<Self> {
|
||||||
let mut bytes = [0; 32];
|
let mut bytes = [0; 32];
|
||||||
fill_random(&mut bytes)?;
|
getrandom::fill(&mut bytes)?;
|
||||||
Ok(Self(bytes))
|
Ok(Self(bytes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,8 +180,7 @@ pub(crate) fn read_bounded_private_file(path: &Path, cap: usize) -> io::Result<V
|
|||||||
|
|
||||||
/// Whether this user owns the file or directory and nobody else may use it.
|
/// Whether this user owns the file or directory and nobody else may use it.
|
||||||
pub(crate) fn is_private(metadata: &Metadata) -> bool {
|
pub(crate) fn is_private(metadata: &Metadata) -> bool {
|
||||||
// SAFETY: geteuid has no preconditions and cannot fail.
|
metadata.uid() == rustix_process::geteuid().as_raw() && metadata.mode() & 0o077 == 0
|
||||||
metadata.uid() == unsafe { libc::geteuid() } && metadata.mode() & 0o077 == 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates `path` as a directory only this user may use, or makes an existing
|
/// Creates `path` as a directory only this user may use, or makes an existing
|
||||||
@@ -215,8 +196,7 @@ pub(crate) fn create_private_directory(path: &Path) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let metadata = fs::symlink_metadata(path)?;
|
let metadata = fs::symlink_metadata(path)?;
|
||||||
// SAFETY: geteuid has no preconditions and cannot fail.
|
if !metadata.is_dir() || metadata.uid() != rustix_process::geteuid().as_raw() {
|
||||||
if !metadata.is_dir() || metadata.uid() != unsafe { libc::geteuid() } {
|
|
||||||
anyhow::bail!("{} is not a no-follow private directory", path.display());
|
anyhow::bail!("{} is not a no-follow private directory", path.display());
|
||||||
}
|
}
|
||||||
fs::set_permissions(path, Permissions::from_mode(0o700))?;
|
fs::set_permissions(path, Permissions::from_mode(0o700))?;
|
||||||
@@ -389,15 +369,10 @@ pub(crate) fn process_start_ticks(pid: u32) -> io::Result<u64> {
|
|||||||
|
|
||||||
pub(crate) fn open_pidfd(pid: u32) -> io::Result<OwnedFd> {
|
pub(crate) fn open_pidfd(pid: u32) -> io::Result<OwnedFd> {
|
||||||
let pid = i32::try_from(pid)
|
let pid = i32::try_from(pid)
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "pid does not fit pid_t"))?;
|
.ok()
|
||||||
// SAFETY: pidfd_open has no pointer arguments. The returned descriptor is
|
.and_then(Pid::from_raw)
|
||||||
// owned by the caller on success.
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "pid does not fit pid_t"))?;
|
||||||
let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
|
Ok(rustix_process::pidfd_open(pid, PidfdFlags::empty())?)
|
||||||
if raw < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
// SAFETY: raw is a new pidfd not wrapped elsewhere.
|
|
||||||
Ok(unsafe { OwnedFd::from_raw_fd(raw as i32) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blocks until the process behind `fd` exits or `timeout` elapses, returning
|
/// Blocks until the process behind `fd` exits or `timeout` elapses, returning
|
||||||
@@ -413,27 +388,14 @@ pub(crate) fn wait_for_pidfd_exit(fd: BorrowedFd<'_>, timeout: Duration) -> io::
|
|||||||
if remaining.is_zero() {
|
if remaining.is_zero() {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
let mut poll_fd = libc::pollfd {
|
let mut pollfds = [PollFd::from_borrowed_fd(fd, PollFlags::IN)];
|
||||||
fd: fd.as_raw_fd(),
|
let remaining = Timespec::try_from(remaining).map_err(io::Error::other)?;
|
||||||
events: libc::POLLIN,
|
match event::poll(&mut pollfds, Some(&remaining)) {
|
||||||
revents: 0,
|
Ok(ready) => return Ok(ready != 0),
|
||||||
};
|
|
||||||
let millis = i32::try_from(remaining.as_millis()).unwrap_or(i32::MAX);
|
|
||||||
// SAFETY: one owned pollfd, valid for the call; the descriptor is
|
|
||||||
// borrowed for at least this long.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut poll_fd, 1, millis) };
|
|
||||||
if ready < 0 {
|
|
||||||
let error = io::Error::last_os_error();
|
|
||||||
// A signal arriving mid-wait is expected on this thread.
|
// A signal arriving mid-wait is expected on this thread.
|
||||||
if error.kind() == io::ErrorKind::Interrupted {
|
Err(Errno::INTR) => {}
|
||||||
continue;
|
Err(errno) => return Err(errno.into()),
|
||||||
}
|
|
||||||
return Err(error);
|
|
||||||
}
|
}
|
||||||
if ready == 0 {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
return Ok(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,15 +421,7 @@ pub(crate) fn validate_pidfd(fd: BorrowedFd<'_>) -> io::Result<()> {
|
|||||||
|
|
||||||
impl BootClock {
|
impl BootClock {
|
||||||
pub(crate) fn now() -> io::Result<BootDeadline> {
|
pub(crate) fn now() -> io::Result<BootDeadline> {
|
||||||
let mut value = libc::timespec {
|
let value = rustix_time::clock_gettime_dynamic(DynamicClockId::Boottime)?;
|
||||||
tv_sec: 0,
|
|
||||||
tv_nsec: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: value is a valid writable timespec and CLOCK_BOOTTIME has no
|
|
||||||
// additional preconditions.
|
|
||||||
if unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, &raw mut value) } != 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
if value.tv_sec < 0 || !(0..1_000_000_000).contains(&value.tv_nsec) {
|
if value.tv_sec < 0 || !(0..1_000_000_000).contains(&value.tv_nsec) {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidData,
|
io::ErrorKind::InvalidData,
|
||||||
@@ -508,17 +462,11 @@ impl BootDeadline {
|
|||||||
|
|
||||||
impl BootDeadlineSource {
|
impl BootDeadlineSource {
|
||||||
pub(crate) fn try_new() -> io::Result<Self> {
|
pub(crate) fn try_new() -> io::Result<Self> {
|
||||||
// SAFETY: timerfd_create returns a new owned descriptor on success.
|
let fd = rustix_time::timerfd_create(
|
||||||
let raw = unsafe {
|
TimerfdClockId::Boottime,
|
||||||
libc::timerfd_create(libc::CLOCK_BOOTTIME, libc::TFD_NONBLOCK | libc::TFD_CLOEXEC)
|
TimerfdFlags::NONBLOCK | TimerfdFlags::CLOEXEC,
|
||||||
};
|
)?;
|
||||||
if raw < 0 {
|
Ok(Self { fd })
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
// SAFETY: raw is a newly created descriptor not wrapped elsewhere.
|
|
||||||
fd: unsafe { OwnedFd::from_raw_fd(raw) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn poll_fd(&self) -> BorrowedFd<'_> {
|
pub(crate) fn poll_fd(&self) -> BorrowedFd<'_> {
|
||||||
@@ -528,82 +476,45 @@ impl BootDeadlineSource {
|
|||||||
pub(crate) fn arm(&self, deadline: BootDeadline) -> io::Result<()> {
|
pub(crate) fn arm(&self, deadline: BootDeadline) -> io::Result<()> {
|
||||||
let seconds = deadline.0 / 1_000_000_000;
|
let seconds = deadline.0 / 1_000_000_000;
|
||||||
let nanos = deadline.0 % 1_000_000_000;
|
let nanos = deadline.0 % 1_000_000_000;
|
||||||
let value = libc::itimerspec {
|
let value = Itimerspec {
|
||||||
it_interval: libc::timespec {
|
it_interval: Timespec::default(),
|
||||||
tv_sec: 0,
|
it_value: Timespec {
|
||||||
tv_nsec: 0,
|
tv_sec: i64::try_from(seconds).map_err(|_| {
|
||||||
},
|
|
||||||
it_value: libc::timespec {
|
|
||||||
tv_sec: libc::time_t::try_from(seconds).map_err(|_| {
|
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "deadline seconds overflow")
|
io::Error::new(io::ErrorKind::InvalidInput, "deadline seconds overflow")
|
||||||
})?,
|
})?,
|
||||||
tv_nsec: libc::c_long::try_from(nanos).map_err(|_| {
|
tv_nsec: i64::try_from(nanos).map_err(|_| {
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "deadline nanos overflow")
|
io::Error::new(io::ErrorKind::InvalidInput, "deadline nanos overflow")
|
||||||
})?,
|
})?,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// SAFETY: value is a fully initialized itimerspec and fd owns a timerfd.
|
rustix_time::timerfd_settime(&self.fd, TimerfdTimerFlags::ABSTIME, &value)?;
|
||||||
if unsafe {
|
|
||||||
libc::timerfd_settime(
|
|
||||||
self.fd.as_raw_fd(),
|
|
||||||
libc::TFD_TIMER_ABSTIME,
|
|
||||||
&raw const value,
|
|
||||||
ptr::null_mut(),
|
|
||||||
)
|
|
||||||
} != 0
|
|
||||||
{
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn disarm(&self) -> io::Result<()> {
|
pub(crate) fn disarm(&self) -> io::Result<()> {
|
||||||
let value = libc::itimerspec {
|
let value = Itimerspec {
|
||||||
it_interval: libc::timespec {
|
it_interval: Timespec::default(),
|
||||||
tv_sec: 0,
|
it_value: Timespec::default(),
|
||||||
tv_nsec: 0,
|
|
||||||
},
|
|
||||||
it_value: libc::timespec {
|
|
||||||
tv_sec: 0,
|
|
||||||
tv_nsec: 0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
// SAFETY: value is initialized and fd owns a timerfd.
|
rustix_time::timerfd_settime(&self.fd, TimerfdTimerFlags::empty(), &value)?;
|
||||||
if unsafe {
|
|
||||||
libc::timerfd_settime(self.fd.as_raw_fd(), 0, &raw const value, ptr::null_mut())
|
|
||||||
} != 0
|
|
||||||
{
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn drain(&self) -> io::Result<bool> {
|
pub(crate) fn drain(&self) -> io::Result<bool> {
|
||||||
let mut expirations = 0_u64;
|
let mut expirations = [0_u8; size_of::<u64>()];
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: expirations is writable and fd remains valid for the read.
|
match rustix_io::read(&self.fd, &mut expirations) {
|
||||||
let result = unsafe {
|
Ok(read) if read == expirations.len() => return Ok(true),
|
||||||
libc::read(
|
Ok(read) => {
|
||||||
self.fd.as_raw_fd(),
|
return Err(io::Error::new(
|
||||||
(&raw mut expirations).cast(),
|
io::ErrorKind::UnexpectedEof,
|
||||||
size_of::<u64>(),
|
format!("timerfd returned a short read ({read} bytes)"),
|
||||||
)
|
));
|
||||||
};
|
}
|
||||||
if result == size_of::<u64>() as isize {
|
Err(Errno::INTR) => {}
|
||||||
return Ok(true);
|
Err(Errno::WOULDBLOCK) => return Ok(false),
|
||||||
|
Err(errno) => return Err(errno.into()),
|
||||||
}
|
}
|
||||||
if result < 0 {
|
|
||||||
let error = io::Error::last_os_error();
|
|
||||||
return match error.kind() {
|
|
||||||
io::ErrorKind::Interrupted => continue,
|
|
||||||
io::ErrorKind::WouldBlock => Ok(false),
|
|
||||||
_ => Err(error),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::UnexpectedEof,
|
|
||||||
format!("timerfd returned a short read ({result} bytes)"),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -611,27 +522,15 @@ impl BootDeadlineSource {
|
|||||||
impl CommandQueueWatcher {
|
impl CommandQueueWatcher {
|
||||||
pub(crate) fn try_new(queue_path: &Path) -> io::Result<Self> {
|
pub(crate) fn try_new(queue_path: &Path) -> io::Result<Self> {
|
||||||
let (queue_fd, queue_identity) = open_nofollow_directory(queue_path)?;
|
let (queue_fd, queue_identity) = open_nofollow_directory(queue_path)?;
|
||||||
// SAFETY: inotify_init1 returns a newly owned descriptor on success.
|
let fd = inotify::init(CreateFlags::NONBLOCK | CreateFlags::CLOEXEC)?;
|
||||||
let raw = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
|
|
||||||
if raw < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
// SAFETY: raw is a new descriptor not wrapped elsewhere.
|
|
||||||
let fd = unsafe { OwnedFd::from_raw_fd(raw) };
|
|
||||||
let path = CString::new(queue_path.as_os_str().as_encoded_bytes())
|
let path = CString::new(queue_path.as_os_str().as_encoded_bytes())
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "queue path contains NUL"))?;
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "queue path contains NUL"))?;
|
||||||
let mask = libc::IN_MOVED_TO
|
// IN_IGNORED and IN_Q_OVERFLOW always arrive, so they are not requested here.
|
||||||
| libc::IN_CREATE
|
let mask = WatchFlags::MOVED_TO
|
||||||
| libc::IN_DELETE_SELF
|
| WatchFlags::CREATE
|
||||||
| libc::IN_MOVE_SELF
|
| WatchFlags::DELETE_SELF
|
||||||
| libc::IN_IGNORED
|
| WatchFlags::MOVE_SELF;
|
||||||
| libc::IN_Q_OVERFLOW;
|
let watch_descriptor = inotify::add_watch(&fd, &path, mask)?;
|
||||||
// SAFETY: path is NUL-terminated and fd owns an inotify instance.
|
|
||||||
let watch_descriptor =
|
|
||||||
unsafe { libc::inotify_add_watch(fd.as_raw_fd(), path.as_ptr(), mask) };
|
|
||||||
if watch_descriptor < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
revalidate_path_identity(queue_path, queue_identity)?;
|
revalidate_path_identity(queue_path, queue_identity)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
fd,
|
fd,
|
||||||
@@ -657,30 +556,20 @@ impl CommandQueueWatcher {
|
|||||||
self.buffered_length = 0;
|
self.buffered_length = 0;
|
||||||
self.buffered_offset = 0;
|
self.buffered_offset = 0;
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: buffer is writable for its full fixed length and fd is
|
match rustix_io::read(&self.fd, &mut self.buffer[..]) {
|
||||||
// a nonblocking inotify descriptor.
|
Ok(0) => {
|
||||||
let read = unsafe {
|
return Err(io::Error::new(
|
||||||
libc::read(
|
io::ErrorKind::UnexpectedEof,
|
||||||
self.fd.as_raw_fd(),
|
"inotify returned EOF",
|
||||||
self.buffer.as_mut_ptr().cast(),
|
));
|
||||||
self.buffer.len(),
|
}
|
||||||
)
|
Ok(read) => {
|
||||||
};
|
self.buffered_length = read;
|
||||||
if read > 0 {
|
return Ok(true);
|
||||||
self.buffered_length = read as usize;
|
}
|
||||||
return Ok(true);
|
Err(Errno::INTR) => {}
|
||||||
}
|
Err(Errno::WOULDBLOCK) => return Ok(false),
|
||||||
if read == 0 {
|
Err(errno) => return Err(errno.into()),
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::UnexpectedEof,
|
|
||||||
"inotify returned EOF",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let error = io::Error::last_os_error();
|
|
||||||
match error.kind() {
|
|
||||||
io::ErrorKind::Interrupted => continue,
|
|
||||||
io::ErrorKind::WouldBlock => return Ok(false),
|
|
||||||
_ => return Err(error),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
use std::os::fd::AsRawFd;
|
use std::os::fd::{AsFd, BorrowedFd};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{env, process};
|
use std::{env, process};
|
||||||
|
|
||||||
|
use rustix::event::{self, PollFd, PollFlags, Timespec};
|
||||||
|
|
||||||
use crate::tray_action::TrayAction;
|
use crate::tray_action::TrayAction;
|
||||||
use crate::{env_vars, test_env, test_temp};
|
use crate::{env_vars, test_env, test_temp};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// Polls for input. Any other readiness fails the test, so a broken descriptor
|
||||||
|
/// cannot pass for silence.
|
||||||
|
fn is_readable(fd: BorrowedFd<'_>, timeout: Duration) -> bool {
|
||||||
|
let mut pollfds = [PollFd::from_borrowed_fd(fd, PollFlags::IN)];
|
||||||
|
let timeout = Timespec::try_from(timeout).unwrap();
|
||||||
|
let ready = event::poll(&mut pollfds, Some(&timeout)).unwrap();
|
||||||
|
let revents = pollfds[0].revents();
|
||||||
|
assert!(
|
||||||
|
ready == 0 || revents == PollFlags::IN,
|
||||||
|
"unexpected readiness {:#x}",
|
||||||
|
revents.bits()
|
||||||
|
);
|
||||||
|
ready == 1
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn production_mode_is_closed_to_published_v2_after_cutover() {
|
fn production_mode_is_closed_to_published_v2_after_cutover() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -71,15 +88,7 @@ fn boottime_deadline_source_arms_absolutely_and_drains_once() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
source.arm(deadline).unwrap();
|
source.arm(deadline).unwrap();
|
||||||
|
|
||||||
let mut poll_fd = libc::pollfd {
|
assert!(is_readable(source.poll_fd(), Duration::from_millis(500)));
|
||||||
fd: source.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: poll_fd remains valid for this bounded poll.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut poll_fd, 1, 500) };
|
|
||||||
assert_eq!(ready, 1);
|
|
||||||
assert_ne!(poll_fd.revents & libc::POLLIN, 0);
|
|
||||||
assert!(source.drain().unwrap());
|
assert!(source.drain().unwrap());
|
||||||
assert!(!source.drain().unwrap());
|
assert!(!source.drain().unwrap());
|
||||||
source.disarm().unwrap();
|
source.disarm().unwrap();
|
||||||
@@ -128,14 +137,7 @@ fn pidfd_wait_times_out_while_alive_and_returns_once_exited() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn self_pidfd_is_not_readable_while_process_is_alive() {
|
fn self_pidfd_is_not_readable_while_process_is_alive() {
|
||||||
let pidfd = linux::open_self_pidfd().unwrap();
|
let pidfd = linux::open_self_pidfd().unwrap();
|
||||||
let mut poll_fd = libc::pollfd {
|
assert!(!is_readable(pidfd.as_fd(), Duration::ZERO));
|
||||||
fd: pidfd.as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: poll_fd remains valid for the zero-timeout observation.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut poll_fd, 1, 0) };
|
|
||||||
assert_eq!(ready, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -189,13 +191,7 @@ fn command_queue_watch_reports_atomic_publication_and_rejects_replacement() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut poll_fd = libc::pollfd {
|
assert!(is_readable(watcher.poll_fd(), Duration::from_millis(500)));
|
||||||
fd: watcher.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: watcher owns the descriptor throughout the bounded poll.
|
|
||||||
assert_eq!(unsafe { libc::poll(&raw mut poll_fd, 1, 500) }, 1);
|
|
||||||
assert!(watcher.drain().unwrap().scan_pending);
|
assert!(watcher.drain().unwrap().scan_pending);
|
||||||
|
|
||||||
fs::rename(&queue, temp.path().join("old-queue")).unwrap();
|
fs::rename(&queue, temp.path().join("old-queue")).unwrap();
|
||||||
|
|||||||
+11
-17
@@ -8,6 +8,9 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use rustix::fs::{self as rustix_fs, RenameFlags};
|
||||||
|
|
||||||
use std::ffi::{CString, OsStr, OsString};
|
use std::ffi::{CString, OsStr, OsString};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||||
@@ -594,23 +597,14 @@ fn metadata_mode(_metadata: &fs::Metadata) -> Option<u32> {
|
|||||||
pub fn rename_no_replace(source: &Path, target: &Path) -> io::Result<()> {
|
pub fn rename_no_replace(source: &Path, target: &Path) -> io::Result<()> {
|
||||||
let source = path_to_cstring(source)?;
|
let source = path_to_cstring(source)?;
|
||||||
let target = path_to_cstring(target)?;
|
let target = path_to_cstring(target)?;
|
||||||
// SAFETY: The C strings are valid, NUL-terminated paths. AT_FDCWD makes both
|
rustix_fs::renameat_with(
|
||||||
// paths relative to the process cwd, matching std::fs::rename path semantics.
|
rustix_fs::CWD,
|
||||||
let result = unsafe {
|
&source,
|
||||||
libc::syscall(
|
rustix_fs::CWD,
|
||||||
libc::SYS_renameat2,
|
&target,
|
||||||
libc::AT_FDCWD,
|
RenameFlags::NOREPLACE,
|
||||||
source.as_ptr(),
|
)
|
||||||
libc::AT_FDCWD,
|
.map_err(io::Error::from)
|
||||||
target.as_ptr(),
|
|
||||||
libc::RENAME_NOREPLACE,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if result == 0 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(io::Error::last_os_error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|||||||
+7
-15
@@ -4,19 +4,11 @@ use std::fs::File;
|
|||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::io::AsRawFd;
|
use rustix::fs::{self as rustix_fs, FlockOperation};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn flock(file: &File, op: libc::c_int) -> io::Result<()> {
|
fn flock(file: &File, operation: FlockOperation) -> io::Result<()> {
|
||||||
let fd = file.as_raw_fd();
|
rustix_fs::flock(file, operation).map_err(io::Error::from)
|
||||||
// SAFETY: `fd` stays open for the borrowed `file`, and flock takes no
|
|
||||||
// pointers or ownership of the descriptor.
|
|
||||||
let result = unsafe { libc::flock(fd, op) };
|
|
||||||
if result == 0 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(io::Error::last_os_error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -30,7 +22,7 @@ fn flock(_file: &File, _op: i32) -> io::Result<()> {
|
|||||||
pub fn lock_shared(file: &File) -> io::Result<()> {
|
pub fn lock_shared(file: &File) -> io::Result<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
flock(file, libc::LOCK_SH)
|
flock(file, FlockOperation::LockShared)
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
@@ -41,7 +33,7 @@ pub fn lock_shared(file: &File) -> io::Result<()> {
|
|||||||
pub fn lock_exclusive(file: &File) -> io::Result<()> {
|
pub fn lock_exclusive(file: &File) -> io::Result<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
flock(file, libc::LOCK_EX)
|
flock(file, FlockOperation::LockExclusive)
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
@@ -52,7 +44,7 @@ pub fn lock_exclusive(file: &File) -> io::Result<()> {
|
|||||||
pub fn try_lock_exclusive(file: &File) -> io::Result<()> {
|
pub fn try_lock_exclusive(file: &File) -> io::Result<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
flock(file, libc::LOCK_EX | libc::LOCK_NB)
|
flock(file, FlockOperation::NonBlockingLockExclusive)
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
@@ -63,7 +55,7 @@ pub fn try_lock_exclusive(file: &File) -> io::Result<()> {
|
|||||||
pub fn unlock(file: &File) -> io::Result<()> {
|
pub fn unlock(file: &File) -> io::Result<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
flock(file, libc::LOCK_UN)
|
flock(file, FlockOperation::Unlock)
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-5
@@ -1,11 +1,10 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::ffi::CString;
|
|
||||||
use std::os::unix;
|
use std::os::unix;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{fs, thread};
|
use std::{fs, thread};
|
||||||
|
|
||||||
use crate::test_env;
|
use crate::{test_env, test_temp};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -314,9 +313,7 @@ fn a_symlinked_override_is_refused() {
|
|||||||
fn a_fifo_override_is_refused_without_waiting_for_a_writer() {
|
fn a_fifo_override_is_refused_without_waiting_for_a_writer() {
|
||||||
let directory = tempfile::tempdir().expect("override directory");
|
let directory = tempfile::tempdir().expect("override directory");
|
||||||
let path = directory.path().join("zh-CN.toml");
|
let path = directory.path().join("zh-CN.toml");
|
||||||
let raw_path = CString::new(path.as_os_str().as_encoded_bytes()).expect("path");
|
test_temp::make_fifo(&path);
|
||||||
// SAFETY: `raw_path` is a valid NUL-terminated path for the call.
|
|
||||||
assert_eq!(unsafe { libc::mkfifo(raw_path.as_ptr(), 0o600) }, 0);
|
|
||||||
|
|
||||||
let (sender, receiver) = mpsc::channel();
|
let (sender, receiver) = mpsc::channel();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
use std::os::fd::{AsRawFd, OwnedFd};
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
@@ -17,6 +17,13 @@ use std::thread::Builder;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
#[cfg(not(test))]
|
||||||
|
use rustix::io as rustix_io;
|
||||||
|
use rustix::io::Errno;
|
||||||
|
use rustix::net::{self, AddressFamily, SocketFlags, SocketType};
|
||||||
|
#[cfg(not(test))]
|
||||||
|
use rustix::process::Signal;
|
||||||
|
use rustix::process::{self as rustix_process, Pid, WaitOptions};
|
||||||
|
|
||||||
use crate::daemon::protocol_v2::ProtocolToken;
|
use crate::daemon::protocol_v2::ProtocolToken;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
@@ -152,9 +159,8 @@ pub(super) fn start() -> Result<ProcessBroker> {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
if let Err(error) = broker.request(BrokerOperation::Ping) {
|
if let Err(error) = broker.request(BrokerOperation::Ping) {
|
||||||
// SAFETY: pid is the raw-clone broker child created above.
|
if let Some(broker_pid) = Pid::from_raw(pid) {
|
||||||
unsafe {
|
let _ = rustix_process::kill_process(broker_pid, Signal::KILL);
|
||||||
libc::kill(pid, libc::SIGKILL);
|
|
||||||
}
|
}
|
||||||
wait_for_broker_process(pid);
|
wait_for_broker_process(pid);
|
||||||
return Err(error).context("process broker exec/authentication handshake failed");
|
return Err(error).context("process broker exec/authentication handshake failed");
|
||||||
@@ -164,37 +170,20 @@ pub(super) fn start() -> Result<ProcessBroker> {
|
|||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
fn duplicate_for_exec(descriptor: &OwnedFd) -> Result<OwnedFd> {
|
fn duplicate_for_exec(descriptor: &OwnedFd) -> Result<OwnedFd> {
|
||||||
// SAFETY: F_DUPFD_CLOEXEC duplicates the live descriptor at or above five.
|
rustix_io::fcntl_dupfd_cloexec(descriptor, 5)
|
||||||
let duplicate = unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 5) };
|
.map_err(io::Error::from)
|
||||||
if duplicate < 0 {
|
.context("failed to stage broker descriptor")
|
||||||
return Err(io::Error::last_os_error()).context("failed to stage broker descriptor");
|
|
||||||
}
|
|
||||||
// SAFETY: fcntl returned a fresh owned descriptor.
|
|
||||||
Ok(unsafe { OwnedFd::from_raw_fd(duplicate) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn socket_pair(label: &str) -> Result<(OwnedFd, OwnedFd)> {
|
fn socket_pair(label: &str) -> Result<(OwnedFd, OwnedFd)> {
|
||||||
let mut sockets = [0; 2];
|
net::socketpair(
|
||||||
// SAFETY: sockets has room for the returned descriptor pair.
|
AddressFamily::UNIX,
|
||||||
if unsafe {
|
SocketType::SEQPACKET,
|
||||||
libc::socketpair(
|
SocketFlags::CLOEXEC,
|
||||||
libc::AF_UNIX,
|
None,
|
||||||
libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC,
|
)
|
||||||
0,
|
.map_err(io::Error::from)
|
||||||
sockets.as_mut_ptr(),
|
.with_context(|| format!("failed to create {label} socketpair"))
|
||||||
)
|
|
||||||
} != 0
|
|
||||||
{
|
|
||||||
return Err(io::Error::last_os_error())
|
|
||||||
.with_context(|| format!("failed to create {label} socketpair"));
|
|
||||||
}
|
|
||||||
// SAFETY: socketpair returned two new descriptors.
|
|
||||||
Ok(unsafe {
|
|
||||||
(
|
|
||||||
OwnedFd::from_raw_fd(sockets[0]),
|
|
||||||
OwnedFd::from_raw_fd(sockets[1]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the broker process exits within `grace`, reaping it if it does.
|
/// Whether the broker process exits within `grace`, reaping it if it does.
|
||||||
@@ -203,20 +192,17 @@ pub(super) fn broker_exits_within(child_pid: libc::pid_t, grace: Duration) -> bo
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(pid) = Pid::from_raw(child_pid) else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
let deadline = Instant::now() + grace;
|
let deadline = Instant::now() + grace;
|
||||||
let mut status = 0;
|
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: child_pid names the raw-clone broker child owned by its guard.
|
match rustix_process::waitpid(Some(pid), WaitOptions::NOHANG) {
|
||||||
let result = unsafe { libc::waitpid(child_pid, &raw mut status, libc::WNOHANG) };
|
Ok(Some(_)) => return true,
|
||||||
if result == child_pid {
|
Ok(None) => {}
|
||||||
return true;
|
Err(Errno::INTR) => continue,
|
||||||
}
|
|
||||||
if result < 0 {
|
|
||||||
if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Already reaped: nothing is left to wait for.
|
// Already reaped: nothing is left to wait for.
|
||||||
return true;
|
Err(_) => return true,
|
||||||
}
|
}
|
||||||
if Instant::now() >= deadline {
|
if Instant::now() >= deadline {
|
||||||
return false;
|
return false;
|
||||||
@@ -229,16 +215,11 @@ pub(super) fn wait_for_broker_process(child_pid: libc::pid_t) {
|
|||||||
if child_pid <= 0 {
|
if child_pid <= 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut status = 0;
|
let Some(pid) = Pid::from_raw(child_pid) else {
|
||||||
loop {
|
return;
|
||||||
// SAFETY: child_pid names the raw-clone broker child owned by its guard.
|
};
|
||||||
let result = unsafe { libc::waitpid(child_pid, &raw mut status, 0) };
|
while matches!(
|
||||||
if result == child_pid {
|
rustix_process::waitpid(Some(pid), WaitOptions::empty()),
|
||||||
break;
|
Err(Errno::INTR)
|
||||||
}
|
) {}
|
||||||
if result < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ use std::collections::VecDeque;
|
|||||||
use std::ffi::{OsStr, OsString};
|
use std::ffi::{OsStr, OsString};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, TryLockError, Weak};
|
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, TryLockError, Weak};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -10,6 +10,9 @@ use std::thread::JoinHandle;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use rustix::io::Errno;
|
||||||
|
use rustix::net::{self, SendFlags};
|
||||||
|
use rustix::process::{self as rustix_process, Pid, Signal};
|
||||||
|
|
||||||
use crate::daemon::protocol_v2::ProtocolId;
|
use crate::daemon::protocol_v2::ProtocolId;
|
||||||
|
|
||||||
@@ -87,8 +90,8 @@ enum ExchangeWait {
|
|||||||
/// The transport policy and watchdog a spawn request carries, kept together so
|
/// The transport policy and watchdog a spawn request carries, kept together so
|
||||||
/// the private spawn path stays inside the argument-count lint.
|
/// the private spawn path stays inside the argument-count lint.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct SpawnOptions {
|
struct SpawnOptions<'descriptor> {
|
||||||
watchdog: Option<RawFd>,
|
watchdog: Option<BorrowedFd<'descriptor>>,
|
||||||
wait: ExchangeWait,
|
wait: ExchangeWait,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +122,7 @@ pub(crate) fn current() -> Result<ProcessBroker> {
|
|||||||
impl Drop for ProcessBrokerGuard {
|
impl Drop for ProcessBrokerGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let was_healthy = self.broker.inner.healthy.swap(false, Ordering::AcqRel);
|
let was_healthy = self.broker.inner.healthy.swap(false, Ordering::AcqRel);
|
||||||
let asked_to_stop = signal_shutdown(self.broker.inner.shutdown.as_raw_fd()).is_ok();
|
let asked_to_stop = signal_shutdown(self.broker.inner.shutdown.as_fd()).is_ok();
|
||||||
// A broker whose exchange failed may be stuck and never read the
|
// A broker whose exchange failed may be stuck and never read the
|
||||||
// shutdown request. Waiting on it without a bound would hang the very
|
// shutdown request. Waiting on it without a bound would hang the very
|
||||||
// exit that lets the service manager start a fresh daemon.
|
// exit that lets the service manager start a fresh daemon.
|
||||||
@@ -129,11 +132,11 @@ impl Drop for ProcessBrokerGuard {
|
|||||||
self.broker.inner.child_pid,
|
self.broker.inner.child_pid,
|
||||||
UNHEALTHY_BROKER_EXIT_GRACE,
|
UNHEALTHY_BROKER_EXIT_GRACE,
|
||||||
));
|
));
|
||||||
if must_kill && self.broker.inner.child_pid > 0 {
|
if must_kill
|
||||||
// SAFETY: child_pid is the broker process owned by this guard.
|
&& self.broker.inner.child_pid > 0
|
||||||
unsafe {
|
&& let Some(pid) = Pid::from_raw(self.broker.inner.child_pid)
|
||||||
libc::kill(self.broker.inner.child_pid, libc::SIGKILL);
|
{
|
||||||
}
|
let _ = rustix_process::kill_process(pid, Signal::KILL);
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if let Some(thread) = self
|
if let Some(thread) = self
|
||||||
@@ -163,29 +166,20 @@ impl Drop for ProcessBrokerGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signal_shutdown(descriptor: RawFd) -> io::Result<()> {
|
fn signal_shutdown(descriptor: BorrowedFd<'_>) -> io::Result<()> {
|
||||||
let byte = [GRACEFUL_SHUTDOWN_BYTE];
|
let byte = [GRACEFUL_SHUTDOWN_BYTE];
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: descriptor is the broker shutdown socket and byte is readable.
|
match net::send(descriptor, &byte, SendFlags::NOSIGNAL) {
|
||||||
let written = unsafe {
|
Ok(1) => return Ok(()),
|
||||||
libc::send(
|
Ok(_) => {
|
||||||
descriptor,
|
return Err(io::Error::new(
|
||||||
byte.as_ptr().cast(),
|
ErrorKind::WriteZero,
|
||||||
byte.len(),
|
"short broker shutdown write",
|
||||||
libc::MSG_NOSIGNAL,
|
));
|
||||||
)
|
}
|
||||||
};
|
Err(Errno::INTR) => {}
|
||||||
if written == 1 {
|
Err(errno) => return Err(errno.into()),
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
if written < 0 && io::Error::last_os_error().kind() == ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Err(if written < 0 {
|
|
||||||
io::Error::last_os_error()
|
|
||||||
} else {
|
|
||||||
io::Error::new(ErrorKind::WriteZero, "short broker shutdown write")
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +247,7 @@ impl ProcessBroker {
|
|||||||
fn request_with_descriptors(
|
fn request_with_descriptors(
|
||||||
&self,
|
&self,
|
||||||
operation: BrokerOperation,
|
operation: BrokerOperation,
|
||||||
descriptors: &[RawFd],
|
descriptors: &[BorrowedFd<'_>],
|
||||||
wait: ExchangeWait,
|
wait: ExchangeWait,
|
||||||
) -> Result<(BrokerOutcome, Vec<OwnedFd>)> {
|
) -> Result<(BrokerOutcome, Vec<OwnedFd>)> {
|
||||||
let request_id = ProtocolId::generate()?.to_string();
|
let request_id = ProtocolId::generate()?.to_string();
|
||||||
@@ -271,9 +265,9 @@ impl ProcessBroker {
|
|||||||
bail!("process broker transport is no longer usable");
|
bail!("process broker transport is no longer usable");
|
||||||
}
|
}
|
||||||
let exchange = (|| -> Result<(BrokerResponse, Vec<OwnedFd>)> {
|
let exchange = (|| -> Result<(BrokerResponse, Vec<OwnedFd>)> {
|
||||||
set_socket_timeout(self.inner.socket.as_raw_fd(), exchange_timeout)?;
|
set_socket_timeout(self.inner.socket.as_fd(), exchange_timeout)?;
|
||||||
send_packet(self.inner.socket.as_raw_fd(), &packet, descriptors)?;
|
send_packet(self.inner.socket.as_fd(), &packet, descriptors)?;
|
||||||
let (packet, descriptors) = recv_packet(self.inner.socket.as_raw_fd())?;
|
let (packet, descriptors) = recv_packet(self.inner.socket.as_fd())?;
|
||||||
let response: BrokerResponse = serde_json::from_slice(&packet)?;
|
let response: BrokerResponse = serde_json::from_slice(&packet)?;
|
||||||
if response.request_id != request_id {
|
if response.request_id != request_id {
|
||||||
bail!("broker response identity mismatch");
|
bail!("broker response identity mismatch");
|
||||||
@@ -368,7 +362,7 @@ impl ProcessBroker {
|
|||||||
let (input, input_descriptor) = encode_blob(input, MAX_INPUT_BYTES)?;
|
let (input, input_descriptor) = encode_blob(input, MAX_INPUT_BYTES)?;
|
||||||
let request_descriptors = input_descriptor
|
let request_descriptors = input_descriptor
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|descriptor| vec![descriptor.as_raw_fd()])
|
.map(|descriptor| vec![descriptor.as_fd()])
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let (outcome, descriptors) = self.request_with_descriptors(
|
let (outcome, descriptors) = self.request_with_descriptors(
|
||||||
BrokerOperation::Run {
|
BrokerOperation::Run {
|
||||||
@@ -428,7 +422,7 @@ impl ProcessBroker {
|
|||||||
let (input, input_descriptor) = encode_blob(input, super::manifest::input_cap(kind))?;
|
let (input, input_descriptor) = encode_blob(input, super::manifest::input_cap(kind))?;
|
||||||
let request_descriptors = input_descriptor
|
let request_descriptors = input_descriptor
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|descriptor| vec![descriptor.as_raw_fd()])
|
.map(|descriptor| vec![descriptor.as_fd()])
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let (outcome, descriptors) = self.request_with_descriptors(
|
let (outcome, descriptors) = self.request_with_descriptors(
|
||||||
BrokerOperation::Publish {
|
BrokerOperation::Publish {
|
||||||
@@ -530,7 +524,7 @@ impl ProcessBroker {
|
|||||||
program: &OsStr,
|
program: &OsStr,
|
||||||
arguments: I,
|
arguments: I,
|
||||||
environment: Vec<(OsString, Option<OsString>)>,
|
environment: Vec<(OsString, Option<OsString>)>,
|
||||||
watchdog: RawFd,
|
watchdog: BorrowedFd<'_>,
|
||||||
) -> Result<BrokerChild>
|
) -> Result<BrokerChild>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
@@ -556,7 +550,7 @@ impl ProcessBroker {
|
|||||||
program: &OsStr,
|
program: &OsStr,
|
||||||
arguments: I,
|
arguments: I,
|
||||||
environment: Vec<(OsString, Option<OsString>)>,
|
environment: Vec<(OsString, Option<OsString>)>,
|
||||||
options: SpawnOptions,
|
options: SpawnOptions<'_>,
|
||||||
) -> Result<BrokerChild>
|
) -> Result<BrokerChild>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::mem::MaybeUninit;
|
use std::os::fd::{AsFd, BorrowedFd};
|
||||||
use std::os::fd::{AsRawFd, RawFd};
|
|
||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio};
|
use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -10,6 +9,10 @@ use std::time::Duration;
|
|||||||
use std::{mem, thread};
|
use std::{mem, thread};
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use rustix::event::{self, PollFd, PollFlags, Timespec};
|
||||||
|
use rustix::fs::{self as rustix_fs, OFlags};
|
||||||
|
use rustix::io::Errno;
|
||||||
|
use rustix::process::{self as rustix_process, Pid, Signal, WaitId, WaitIdOptions};
|
||||||
|
|
||||||
use crate::daemon::protocol_v2::{BootClock, BootDeadline};
|
use crate::daemon::protocol_v2::{BootClock, BootDeadline};
|
||||||
|
|
||||||
@@ -25,7 +28,10 @@ use super::wire::{HelperKind, MAX_STDERR_BYTES, OutputMode, STDOUT_CAP_EXCEEDED}
|
|||||||
const PIPE_DRAIN_GRACE: Duration = Duration::from_secs(2);
|
const PIPE_DRAIN_GRACE: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
/// How often a pipe thread waiting on an idle pipe checks whether to give up.
|
/// How often a pipe thread waiting on an idle pipe checks whether to give up.
|
||||||
const PIPE_POLL_INTERVAL_MS: libc::c_int = 20;
|
const PIPE_POLL_INTERVAL: Timespec = Timespec {
|
||||||
|
tv_sec: 0,
|
||||||
|
tv_nsec: 20_000_000,
|
||||||
|
};
|
||||||
|
|
||||||
const PIPE_HELD_OPEN: &str = "broker helper pipe is still open after the helper exited";
|
const PIPE_HELD_OPEN: &str = "broker helper pipe is still open after the helper exited";
|
||||||
|
|
||||||
@@ -63,12 +69,9 @@ pub(super) fn terminate_owned_children(children: &mut BTreeMap<String, Child>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn kill_child_process_group(child: &mut Child) {
|
pub(super) fn kill_child_process_group(child: &mut Child) {
|
||||||
let pid = child.id();
|
// Broker children use a fresh process group equal to their PID.
|
||||||
if let Ok(group) = i32::try_from(pid) {
|
if rustix_process::kill_process_group(Pid::from_child(child), Signal::KILL).is_ok() {
|
||||||
// SAFETY: broker children use a fresh process group equal to their PID.
|
return;
|
||||||
if unsafe { libc::kill(-group, libc::SIGKILL) } == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
}
|
}
|
||||||
@@ -145,48 +148,28 @@ impl Drop for OwnedProcess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn child_status_unreaped(child: &Child) -> io::Result<Option<i32>> {
|
fn child_status_unreaped(child: &Child) -> io::Result<Option<i32>> {
|
||||||
let mut info = MaybeUninit::<libc::siginfo_t>::zeroed();
|
|
||||||
// WNOWAIT pins the zombie leader and therefore its process-group identity.
|
// WNOWAIT pins the zombie leader and therefore its process-group identity.
|
||||||
// SAFETY: info is writable and child is owned by this broker.
|
let options = WaitIdOptions::EXITED | WaitIdOptions::NOHANG | WaitIdOptions::NOWAIT;
|
||||||
if unsafe {
|
let Some(status) = rustix_process::waitid(WaitId::Pid(Pid::from_child(child)), options)? else {
|
||||||
libc::waitid(
|
|
||||||
libc::P_PID,
|
|
||||||
child.id(),
|
|
||||||
info.as_mut_ptr(),
|
|
||||||
libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
|
|
||||||
)
|
|
||||||
} != 0
|
|
||||||
{
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
// SAFETY: waitid initialized info after success.
|
|
||||||
let info = unsafe { info.assume_init() };
|
|
||||||
// SAFETY: info was zeroed before the call and waitid only writes a SIGCHLD
|
|
||||||
// record into it, so the pid field it reads is initialized; it stays zero
|
|
||||||
// when WNOHANG found no waitable child.
|
|
||||||
if unsafe { info.si_pid() } == 0 {
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
|
||||||
// SAFETY: CLD terminal records initialize si_status.
|
|
||||||
let detail = unsafe { info.si_status() };
|
|
||||||
let status = match info.si_code {
|
|
||||||
libc::CLD_EXITED => detail,
|
|
||||||
libc::CLD_KILLED | libc::CLD_DUMPED => 128_i32.saturating_add(detail),
|
|
||||||
_ => {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"waitid returned a nonterminal child status",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
Ok(Some(status))
|
if let Some(code) = status.exit_status() {
|
||||||
|
return Ok(Some(code));
|
||||||
|
}
|
||||||
|
if let Some(signal) = status.terminating_signal() {
|
||||||
|
return Ok(Some(128_i32.saturating_add(signal)));
|
||||||
|
}
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"waitid returned a nonterminal child status",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn publish_bounded(
|
pub(super) fn publish_bounded(
|
||||||
mut command: Command,
|
mut command: Command,
|
||||||
input: Vec<u8>,
|
input: Vec<u8>,
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
shutdown_fd: RawFd,
|
shutdown_fd: BorrowedFd<'static>,
|
||||||
) -> Result<PublishOutput> {
|
) -> Result<PublishOutput> {
|
||||||
if shutdown_requested(shutdown_fd)? {
|
if shutdown_requested(shutdown_fd)? {
|
||||||
return Err(anyhow!("broker publication cancelled during shutdown"));
|
return Err(anyhow!("broker publication cancelled during shutdown"));
|
||||||
@@ -254,17 +237,10 @@ fn write_input_until(
|
|||||||
mut stdin: ChildStdin,
|
mut stdin: ChildStdin,
|
||||||
input: &[u8],
|
input: &[u8],
|
||||||
deadline: BootDeadline,
|
deadline: BootDeadline,
|
||||||
shutdown_fd: RawFd,
|
shutdown_fd: BorrowedFd<'static>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let descriptor = stdin.as_raw_fd();
|
let flags = rustix_fs::fcntl_getfl(&stdin)?;
|
||||||
// SAFETY: fcntl reads and updates descriptor-local flags.
|
rustix_fs::fcntl_setfl(&stdin, flags | OFlags::NONBLOCK)?;
|
||||||
let flags = unsafe { libc::fcntl(descriptor, libc::F_GETFL) };
|
|
||||||
// SAFETY: `stdin` keeps the descriptor open, and F_SETFL only adds
|
|
||||||
// O_NONBLOCK to the flags just read from it.
|
|
||||||
if flags < 0 || unsafe { libc::fcntl(descriptor, libc::F_SETFL, flags | libc::O_NONBLOCK) } != 0
|
|
||||||
{
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
while offset < input.len() {
|
while offset < input.len() {
|
||||||
if shutdown_requested(shutdown_fd)? {
|
if shutdown_requested(shutdown_fd)? {
|
||||||
@@ -298,7 +274,7 @@ pub(super) fn run_bounded(
|
|||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
output_cap: usize,
|
output_cap: usize,
|
||||||
output_mode: OutputMode,
|
output_mode: OutputMode,
|
||||||
shutdown_fd: RawFd,
|
shutdown_fd: BorrowedFd<'static>,
|
||||||
) -> Result<BoundedOutput> {
|
) -> Result<BoundedOutput> {
|
||||||
if shutdown_requested(shutdown_fd)? {
|
if shutdown_requested(shutdown_fd)? {
|
||||||
return Err(anyhow!("broker helper cancelled during shutdown"));
|
return Err(anyhow!("broker helper cancelled during shutdown"));
|
||||||
@@ -421,7 +397,7 @@ struct AbandonablePipe<P> {
|
|||||||
abandon: Arc<AtomicBool>,
|
abandon: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: AsRawFd> AbandonablePipe<P> {
|
impl<P: AsFd> AbandonablePipe<P> {
|
||||||
fn new(pipe: P, abandon: &Arc<AtomicBool>) -> Self {
|
fn new(pipe: P, abandon: &Arc<AtomicBool>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pipe,
|
pipe,
|
||||||
@@ -429,42 +405,31 @@ impl<P: AsRawFd> AbandonablePipe<P> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_until_ready(&self, events: libc::c_short) -> io::Result<()> {
|
fn wait_until_ready(&self, events: PollFlags) -> io::Result<()> {
|
||||||
let mut pollfd = libc::pollfd {
|
let mut pollfds = [PollFd::new(&self.pipe, events)];
|
||||||
fd: self.pipe.as_raw_fd(),
|
|
||||||
events,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
loop {
|
loop {
|
||||||
if self.abandon.load(Ordering::Acquire) {
|
if self.abandon.load(Ordering::Acquire) {
|
||||||
return Err(io::Error::new(io::ErrorKind::TimedOut, PIPE_HELD_OPEN));
|
return Err(io::Error::new(io::ErrorKind::TimedOut, PIPE_HELD_OPEN));
|
||||||
}
|
}
|
||||||
// SAFETY: pollfd points to one initialized entry, and the pipe
|
match event::poll(&mut pollfds, Some(&PIPE_POLL_INTERVAL)) {
|
||||||
// stays open for as long as `self` owns it.
|
Ok(0) | Err(Errno::INTR) => {}
|
||||||
let result = unsafe { libc::poll(&raw mut pollfd, 1, PIPE_POLL_INTERVAL_MS) };
|
Ok(_) => return Ok(()),
|
||||||
if result > 0 {
|
Err(errno) => return Err(errno.into()),
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if result < 0 {
|
|
||||||
let error = io::Error::last_os_error();
|
|
||||||
if error.kind() != io::ErrorKind::Interrupted {
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: Read + AsRawFd> Read for AbandonablePipe<P> {
|
impl<P: Read + AsFd> Read for AbandonablePipe<P> {
|
||||||
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
|
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||||
self.wait_until_ready(libc::POLLIN)?;
|
self.wait_until_ready(PollFlags::IN)?;
|
||||||
self.pipe.read(buffer)
|
self.pipe.read(buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: Write + AsRawFd> Write for AbandonablePipe<P> {
|
impl<P: Write + AsFd> Write for AbandonablePipe<P> {
|
||||||
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
|
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
|
||||||
self.wait_until_ready(libc::POLLOUT)?;
|
self.wait_until_ready(PollFlags::OUT)?;
|
||||||
// A writable pipe has room for at least PIPE_BUF bytes, so a write of
|
// A writable pipe has room for at least PIPE_BUF bytes, so a write of
|
||||||
// no more than that cannot block on a reader that stopped reading.
|
// no more than that cannot block on a reader that stopped reading.
|
||||||
self.pipe.write(&buffer[..buffer.len().min(libc::PIPE_BUF)])
|
self.pipe.write(&buffer[..buffer.len().min(libc::PIPE_BUF)])
|
||||||
|
|||||||
+80
-113
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::{BTreeMap, VecDeque};
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
|
||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
use std::process::{Child, ExitCode, Stdio};
|
use std::process::{Child, ExitCode, Stdio};
|
||||||
use std::thread::Builder;
|
use std::thread::Builder;
|
||||||
@@ -7,6 +7,9 @@ use std::time::Duration;
|
|||||||
use std::{env, io, mem};
|
use std::{env, io, mem};
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use rustix::event::{self, PollFd, PollFlags};
|
||||||
|
use rustix::io::{self as rustix_io, Errno, FdFlags};
|
||||||
|
use rustix::process::{self as rustix_process, Pid, Signal};
|
||||||
|
|
||||||
use crate::daemon::protocol_v2::ProtocolId;
|
use crate::daemon::protocol_v2::ProtocolId;
|
||||||
use crate::env_vars;
|
use crate::env_vars;
|
||||||
@@ -41,10 +44,17 @@ pub(crate) fn run_internal_broker_if_requested() -> Option<ExitCode> {
|
|||||||
{
|
{
|
||||||
return Some(ExitCode::from(126));
|
return Some(ExitCode::from(126));
|
||||||
}
|
}
|
||||||
|
// SAFETY: both numbers were just checked to be open broker sockets, and
|
||||||
|
// nothing in this process closes them before it exits.
|
||||||
|
let (fd, shutdown_fd) = unsafe {
|
||||||
|
(
|
||||||
|
BorrowedFd::borrow_raw(fd),
|
||||||
|
BorrowedFd::borrow_raw(shutdown_fd),
|
||||||
|
)
|
||||||
|
};
|
||||||
// Restore CLOEXEC before any runtime helper can inherit broker internals.
|
// Restore CLOEXEC before any runtime helper can inherit broker internals.
|
||||||
for descriptor in [fd, shutdown_fd] {
|
for descriptor in [fd, shutdown_fd] {
|
||||||
// SAFETY: both descriptors were validated as inherited broker channels.
|
if rustix_io::fcntl_setfd(descriptor, FdFlags::CLOEXEC).is_err() {
|
||||||
if unsafe { libc::fcntl(descriptor, libc::F_SETFD, libc::FD_CLOEXEC) } != 0 {
|
|
||||||
return Some(ExitCode::from(126));
|
return Some(ExitCode::from(126));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,7 +73,9 @@ pub(crate) fn run_internal_broker_if_requested() -> Option<ExitCode> {
|
|||||||
fn validate_broker_socket(fd: RawFd) -> io::Result<()> {
|
fn validate_broker_socket(fd: RawFd) -> io::Result<()> {
|
||||||
let mut socket_type = 0_i32;
|
let mut socket_type = 0_i32;
|
||||||
let mut length = mem::size_of::<i32>() as libc::socklen_t;
|
let mut length = mem::size_of::<i32>() as libc::socklen_t;
|
||||||
// SAFETY: the output slots are valid for getsockopt.
|
// SAFETY: the output slots are valid for getsockopt, which also rejects a
|
||||||
|
// number that is not an open descriptor. A borrowed descriptor cannot be
|
||||||
|
// used here because it requires the number to be open already.
|
||||||
if unsafe {
|
if unsafe {
|
||||||
libc::getsockopt(
|
libc::getsockopt(
|
||||||
fd,
|
fd,
|
||||||
@@ -85,7 +97,11 @@ fn validate_broker_socket(fd: RawFd) -> io::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn broker_loop(socket: RawFd, shutdown_fd: RawFd, token: &str) -> Result<()> {
|
fn broker_loop(
|
||||||
|
socket: BorrowedFd<'static>,
|
||||||
|
shutdown_fd: BorrowedFd<'static>,
|
||||||
|
token: &str,
|
||||||
|
) -> Result<()> {
|
||||||
let mut ownership = BrokerOwnership::default();
|
let mut ownership = BrokerOwnership::default();
|
||||||
loop {
|
loop {
|
||||||
if wait_for_request(socket, shutdown_fd)? == BrokerWake::Shutdown {
|
if wait_for_request(socket, shutdown_fd)? == BrokerWake::Shutdown {
|
||||||
@@ -139,7 +155,7 @@ fn broker_loop(socket: RawFd, shutdown_fd: RawFd, token: &str) -> Result<()> {
|
|||||||
let response_descriptors = wire_response
|
let response_descriptors = wire_response
|
||||||
.descriptors
|
.descriptors
|
||||||
.iter()
|
.iter()
|
||||||
.map(AsRawFd::as_raw_fd)
|
.map(AsFd::as_fd)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
send_packet(socket, &bytes, &response_descriptors)?;
|
send_packet(socket, &bytes, &response_descriptors)?;
|
||||||
if take_graceful_shutdown_signal(shutdown_fd)? {
|
if take_graceful_shutdown_signal(shutdown_fd)? {
|
||||||
@@ -155,39 +171,27 @@ enum BrokerWake {
|
|||||||
Shutdown,
|
Shutdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_request(socket: RawFd, shutdown_fd: RawFd) -> io::Result<BrokerWake> {
|
fn wait_for_request(socket: BorrowedFd<'_>, shutdown_fd: BorrowedFd<'_>) -> io::Result<BrokerWake> {
|
||||||
let mut descriptors = [
|
let mut descriptors = [
|
||||||
libc::pollfd {
|
PollFd::from_borrowed_fd(socket, PollFlags::IN),
|
||||||
fd: socket,
|
PollFd::from_borrowed_fd(shutdown_fd, PollFlags::IN),
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
},
|
|
||||||
libc::pollfd {
|
|
||||||
fd: shutdown_fd,
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
let unusable = PollFlags::HUP | PollFlags::ERR | PollFlags::NVAL;
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: descriptors points to two initialized pollfd entries.
|
match event::poll(&mut descriptors, None) {
|
||||||
let result = unsafe { libc::poll(descriptors.as_mut_ptr(), descriptors.len() as _, -1) };
|
Ok(_) => {}
|
||||||
if result < 0 {
|
Err(Errno::INTR) => continue,
|
||||||
let error = io::Error::last_os_error();
|
Err(errno) => return Err(errno.into()),
|
||||||
if error.kind() == io::ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Err(error);
|
|
||||||
}
|
}
|
||||||
if descriptors[1].revents & libc::POLLIN != 0 && take_graceful_shutdown_signal(shutdown_fd)?
|
let socket_events = descriptors[0].revents();
|
||||||
{
|
let shutdown_events = descriptors[1].revents();
|
||||||
|
if shutdown_events.contains(PollFlags::IN) && take_graceful_shutdown_signal(shutdown_fd)? {
|
||||||
return Ok(BrokerWake::Shutdown);
|
return Ok(BrokerWake::Shutdown);
|
||||||
}
|
}
|
||||||
if descriptors[0].revents & libc::POLLIN != 0 {
|
if socket_events.contains(PollFlags::IN) {
|
||||||
return Ok(BrokerWake::Request);
|
return Ok(BrokerWake::Request);
|
||||||
}
|
}
|
||||||
if descriptors[0].revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0
|
if socket_events.intersects(unusable) || shutdown_events.intersects(unusable) {
|
||||||
|| descriptors[1].revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0
|
|
||||||
{
|
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::BrokenPipe,
|
io::ErrorKind::BrokenPipe,
|
||||||
"broker control channel became unusable",
|
"broker control channel became unusable",
|
||||||
@@ -198,6 +202,14 @@ fn wait_for_request(socket: RawFd, shutdown_fd: RawFd) -> io::Result<BrokerWake>
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(super) fn run_loop_for_test(socket: RawFd, shutdown_fd: RawFd, token: &str) -> Result<()> {
|
pub(super) fn run_loop_for_test(socket: RawFd, shutdown_fd: RawFd, token: &str) -> Result<()> {
|
||||||
|
// SAFETY: the test broker thread owns both descriptors and keeps them open
|
||||||
|
// until this loop returns.
|
||||||
|
let (socket, shutdown_fd) = unsafe {
|
||||||
|
(
|
||||||
|
BorrowedFd::borrow_raw(socket),
|
||||||
|
BorrowedFd::borrow_raw(shutdown_fd),
|
||||||
|
)
|
||||||
|
};
|
||||||
broker_loop(socket, shutdown_fd, token)
|
broker_loop(socket, shutdown_fd, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +254,7 @@ fn handle_operation(
|
|||||||
operation: BrokerOperation,
|
operation: BrokerOperation,
|
||||||
descriptors: &mut VecDeque<OwnedFd>,
|
descriptors: &mut VecDeque<OwnedFd>,
|
||||||
ownership: &mut BrokerOwnership,
|
ownership: &mut BrokerOwnership,
|
||||||
shutdown_fd: RawFd,
|
shutdown_fd: BorrowedFd<'static>,
|
||||||
) -> Result<BrokerWireResponse> {
|
) -> Result<BrokerWireResponse> {
|
||||||
if shutdown_requested(shutdown_fd)? {
|
if shutdown_requested(shutdown_fd)? {
|
||||||
bail!("broker operation cancelled during shutdown");
|
bail!("broker operation cancelled during shutdown");
|
||||||
@@ -350,20 +362,20 @@ fn handle_operation(
|
|||||||
),
|
),
|
||||||
BrokerOperation::Signal { handle, signal } => {
|
BrokerOperation::Signal { handle, signal } => {
|
||||||
reject_descriptors(descriptors)?;
|
reject_descriptors(descriptors)?;
|
||||||
if !matches!(
|
let signal = match signal {
|
||||||
signal,
|
libc::SIGUSR1 => Signal::USR1,
|
||||||
libc::SIGUSR1 | libc::SIGUSR2 | libc::SIGTERM | libc::SIGKILL
|
libc::SIGUSR2 => Signal::USR2,
|
||||||
) {
|
libc::SIGTERM => Signal::TERM,
|
||||||
bail!("signal is not allowed by broker manifest");
|
libc::SIGKILL => Signal::KILL,
|
||||||
}
|
_ => bail!("signal is not allowed by broker manifest"),
|
||||||
|
};
|
||||||
let child = ownership
|
let child = ownership
|
||||||
.children
|
.children
|
||||||
.get(&handle)
|
.get(&handle)
|
||||||
.ok_or_else(|| anyhow!("unknown broker child handle"))?;
|
.ok_or_else(|| anyhow!("unknown broker child handle"))?;
|
||||||
// SAFETY: the broker retains the exact unreaped child handle.
|
rustix_process::kill_process(Pid::from_child(child), signal)
|
||||||
if unsafe { libc::kill(child.id() as i32, signal) } != 0 {
|
.map_err(io::Error::from)
|
||||||
return Err(io::Error::last_os_error()).context("broker child signal failed");
|
.context("broker child signal failed")?;
|
||||||
}
|
|
||||||
Ok(wire_outcome(BrokerOutcome::Acknowledged))
|
Ok(wire_outcome(BrokerOutcome::Acknowledged))
|
||||||
}
|
}
|
||||||
BrokerOperation::TryWait { handle } => {
|
BrokerOperation::TryWait { handle } => {
|
||||||
@@ -409,7 +421,7 @@ fn spawn_helper(
|
|||||||
request: SpawnRequest,
|
request: SpawnRequest,
|
||||||
descriptors: &mut VecDeque<OwnedFd>,
|
descriptors: &mut VecDeque<OwnedFd>,
|
||||||
children: &mut BTreeMap<String, Child>,
|
children: &mut BTreeMap<String, Child>,
|
||||||
shutdown_fd: RawFd,
|
shutdown_fd: BorrowedFd<'static>,
|
||||||
) -> Result<BrokerWireResponse> {
|
) -> Result<BrokerWireResponse> {
|
||||||
let SpawnRequest {
|
let SpawnRequest {
|
||||||
kind,
|
kind,
|
||||||
@@ -504,22 +516,17 @@ fn reject_descriptors(descriptors: &VecDeque<OwnedFd>) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_cloexec(descriptor: &OwnedFd, enabled: bool) -> Result<()> {
|
fn set_cloexec(descriptor: &OwnedFd, enabled: bool) -> Result<()> {
|
||||||
// SAFETY: fcntl reads and updates descriptor-local flags.
|
let current = rustix_io::fcntl_getfd(descriptor)
|
||||||
let current = unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_GETFD) };
|
.map_err(io::Error::from)
|
||||||
if current < 0 {
|
.context("failed to read descriptor flags")?;
|
||||||
return Err(io::Error::last_os_error()).context("failed to read descriptor flags");
|
|
||||||
}
|
|
||||||
let updated = if enabled {
|
let updated = if enabled {
|
||||||
current | libc::FD_CLOEXEC
|
current | FdFlags::CLOEXEC
|
||||||
} else {
|
} else {
|
||||||
current & !libc::FD_CLOEXEC
|
current - FdFlags::CLOEXEC
|
||||||
};
|
};
|
||||||
// SAFETY: `descriptor` is borrowed open for the call, and `updated` only
|
rustix_io::fcntl_setfd(descriptor, updated)
|
||||||
// changes FD_CLOEXEC in the flags just read from it.
|
.map_err(io::Error::from)
|
||||||
if unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_SETFD, updated) } != 0 {
|
.context("failed to update descriptor flags")
|
||||||
return Err(io::Error::last_os_error()).context("failed to update descriptor flags");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wire_outcome(outcome: BrokerOutcome) -> BrokerWireResponse {
|
fn wire_outcome(outcome: BrokerOutcome) -> BrokerWireResponse {
|
||||||
@@ -551,30 +558,18 @@ fn canonical_lower_hex(value: &str, length: usize) -> bool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
use rustix::net::{self, AddressFamily, SendFlags, SocketFlags, SocketType};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::os::fd::FromRawFd;
|
|
||||||
|
|
||||||
fn test_socket_pair() -> (OwnedFd, OwnedFd) {
|
fn test_socket_pair() -> (OwnedFd, OwnedFd) {
|
||||||
let mut sockets = [0; 2];
|
net::socketpair(
|
||||||
assert_eq!(
|
AddressFamily::UNIX,
|
||||||
// SAFETY: sockets has room for both returned descriptors.
|
SocketType::SEQPACKET,
|
||||||
unsafe {
|
SocketFlags::CLOEXEC,
|
||||||
libc::socketpair(
|
None,
|
||||||
libc::AF_UNIX,
|
)
|
||||||
libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC,
|
.unwrap()
|
||||||
0,
|
|
||||||
sockets.as_mut_ptr(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
0
|
|
||||||
);
|
|
||||||
// SAFETY: socketpair returned two fresh owned descriptors.
|
|
||||||
unsafe {
|
|
||||||
(
|
|
||||||
OwnedFd::from_raw_fd(sockets[0]),
|
|
||||||
OwnedFd::from_raw_fd(sockets[1]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -583,8 +578,7 @@ mod tests {
|
|||||||
let (shutdown_writer, shutdown_reader) = test_socket_pair();
|
let (shutdown_writer, shutdown_reader) = test_socket_pair();
|
||||||
drop(shutdown_writer);
|
drop(shutdown_writer);
|
||||||
|
|
||||||
let Err(error) = wait_for_request(control_reader.as_raw_fd(), shutdown_reader.as_raw_fd())
|
let Err(error) = wait_for_request(control_reader.as_fd(), shutdown_reader.as_fd()) else {
|
||||||
else {
|
|
||||||
panic!("hangup must unwind through destructive broker cleanup");
|
panic!("hangup must unwind through destructive broker cleanup");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -597,21 +591,13 @@ mod tests {
|
|||||||
let (shutdown_writer, shutdown_reader) = test_socket_pair();
|
let (shutdown_writer, shutdown_reader) = test_socket_pair();
|
||||||
let signal = [super::super::transport::GRACEFUL_SHUTDOWN_BYTE];
|
let signal = [super::super::transport::GRACEFUL_SHUTDOWN_BYTE];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
// SAFETY: both the descriptor and one-byte buffer are valid.
|
net::send(&shutdown_writer, &signal, SendFlags::NOSIGNAL).unwrap(),
|
||||||
unsafe {
|
|
||||||
libc::send(
|
|
||||||
shutdown_writer.as_raw_fd(),
|
|
||||||
signal.as_ptr().cast(),
|
|
||||||
signal.len(),
|
|
||||||
libc::MSG_NOSIGNAL,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
drop(shutdown_writer);
|
drop(shutdown_writer);
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
wait_for_request(control_reader.as_raw_fd(), shutdown_reader.as_raw_fd()),
|
wait_for_request(control_reader.as_fd(), shutdown_reader.as_fd()),
|
||||||
Ok(BrokerWake::Shutdown)
|
Ok(BrokerWake::Shutdown)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -622,20 +608,11 @@ mod tests {
|
|||||||
let (shutdown_writer, shutdown_reader) = test_socket_pair();
|
let (shutdown_writer, shutdown_reader) = test_socket_pair();
|
||||||
let invalid = [2_u8];
|
let invalid = [2_u8];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
// SAFETY: both the descriptor and one-byte buffer are valid.
|
net::send(&shutdown_writer, &invalid, SendFlags::NOSIGNAL).unwrap(),
|
||||||
unsafe {
|
|
||||||
libc::send(
|
|
||||||
shutdown_writer.as_raw_fd(),
|
|
||||||
invalid.as_ptr().cast(),
|
|
||||||
invalid.len(),
|
|
||||||
libc::MSG_NOSIGNAL,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
let Err(error) = wait_for_request(control_reader.as_raw_fd(), shutdown_reader.as_raw_fd())
|
let Err(error) = wait_for_request(control_reader.as_fd(), shutdown_reader.as_fd()) else {
|
||||||
else {
|
|
||||||
panic!("invalid packet must not authorize ownership release");
|
panic!("invalid packet must not authorize ownership release");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -648,20 +625,11 @@ mod tests {
|
|||||||
let (shutdown_writer, shutdown_reader) = test_socket_pair();
|
let (shutdown_writer, shutdown_reader) = test_socket_pair();
|
||||||
let invalid = [super::super::transport::GRACEFUL_SHUTDOWN_BYTE; 2];
|
let invalid = [super::super::transport::GRACEFUL_SHUTDOWN_BYTE; 2];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
// SAFETY: both the descriptor and two-byte buffer are valid.
|
net::send(&shutdown_writer, &invalid, SendFlags::NOSIGNAL).unwrap(),
|
||||||
unsafe {
|
|
||||||
libc::send(
|
|
||||||
shutdown_writer.as_raw_fd(),
|
|
||||||
invalid.as_ptr().cast(),
|
|
||||||
invalid.len(),
|
|
||||||
libc::MSG_NOSIGNAL,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
let Err(error) = wait_for_request(control_reader.as_raw_fd(), shutdown_reader.as_raw_fd())
|
let Err(error) = wait_for_request(control_reader.as_fd(), shutdown_reader.as_fd()) else {
|
||||||
else {
|
|
||||||
panic!("oversized packet must not authorize ownership release");
|
panic!("oversized packet must not authorize ownership release");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -681,7 +649,6 @@ mod tests {
|
|||||||
|
|
||||||
drop(ownership);
|
drop(ownership);
|
||||||
|
|
||||||
// SAFETY: signal zero only probes the test-owned provider after teardown.
|
assert!(rustix_process::test_kill_process(Pid::from_raw(pid).unwrap()).is_err());
|
||||||
assert_ne!(unsafe { libc::kill(pid, 0) }, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+34
-51
@@ -1,5 +1,5 @@
|
|||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::os::fd::AsRawFd;
|
use std::os::fd::AsFd;
|
||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -7,10 +7,23 @@ use std::sync::mpsc;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{env, fs, iter, thread};
|
use std::{env, fs, iter, thread};
|
||||||
|
|
||||||
|
use rustix::net::{self, Shutdown};
|
||||||
|
use rustix::process::{self as rustix_process, Pid, Signal};
|
||||||
|
|
||||||
use crate::daemon::protocol_v2;
|
use crate::daemon::protocol_v2;
|
||||||
use crate::test_fake_helper::Role;
|
use crate::test_fake_helper::Role;
|
||||||
use crate::{env_vars, process_broker, test_env, test_fake_helper, test_temp};
|
use crate::{env_vars, process_broker, test_env, test_fake_helper, test_temp};
|
||||||
|
|
||||||
|
fn process_exists(pid: libc::pid_t) -> bool {
|
||||||
|
Pid::from_raw(pid).is_some_and(|pid| rustix_process::test_kill_process(pid).is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kill_for_cleanup(pid: libc::pid_t) {
|
||||||
|
if let Some(pid) = Pid::from_raw(pid) {
|
||||||
|
let _ = rustix_process::kill_process(pid, Signal::KILL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn wire_arguments(arguments: &[&str]) -> Vec<super::wire::OsWire> {
|
fn wire_arguments(arguments: &[&str]) -> Vec<super::wire::OsWire> {
|
||||||
@@ -28,10 +41,7 @@ fn release_test_provider(release_path: &Path, proof_path: &Path, provider_pid: i
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if Instant::now() >= deadline {
|
if Instant::now() >= deadline {
|
||||||
// SAFETY: cleanup keeps a failing regression test from leaking its provider.
|
kill_for_cleanup(provider_pid);
|
||||||
unsafe {
|
|
||||||
libc::kill(provider_pid, libc::SIGKILL);
|
|
||||||
}
|
|
||||||
panic!("successful provider could not act after normal broker shutdown");
|
panic!("successful provider could not act after normal broker shutdown");
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(5));
|
thread::sleep(Duration::from_millis(5));
|
||||||
@@ -540,8 +550,7 @@ fn process_group_guard_cleans_up_before_ownership_transfer() {
|
|||||||
|
|
||||||
drop(child);
|
drop(child);
|
||||||
|
|
||||||
// SAFETY: signal zero only probes the test-owned helper PID after guard cleanup.
|
assert!(!process_exists(pid));
|
||||||
assert_ne!(unsafe { libc::kill(pid, 0) }, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -762,8 +771,7 @@ fn broker_guard_preempts_an_active_operation_and_kills_its_group() {
|
|||||||
"broker shutdown waited for the active operation"
|
"broker shutdown waited for the active operation"
|
||||||
);
|
);
|
||||||
assert!(run.join().unwrap().is_err());
|
assert!(run.join().unwrap().is_err());
|
||||||
// SAFETY: signal zero only probes the test-owned helper PID.
|
assert!(!process_exists(helper_pid));
|
||||||
assert_ne!(unsafe { libc::kill(helper_pid, 0) }, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -778,7 +786,7 @@ fn owned_child_inherits_daemon_pidfd_without_leaking_broker_copy() {
|
|||||||
OsStr::new("sleep"),
|
OsStr::new("sleep"),
|
||||||
[OsStr::new("30")],
|
[OsStr::new("30")],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
watchdog.as_raw_fd(),
|
watchdog.as_fd(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let inherited_pidfd = fs::read_dir(format!("/proc/{}/fd", child.id()))
|
let inherited_pidfd = fs::read_dir(format!("/proc/{}/fd", child.id()))
|
||||||
@@ -891,10 +899,7 @@ fn a_descendant_outside_the_helper_group_cannot_hold_the_broker_on_its_pipes() {
|
|||||||
.ok()
|
.ok()
|
||||||
.and_then(|pid| pid.trim().parse::<libc::pid_t>().ok())
|
.and_then(|pid| pid.trim().parse::<libc::pid_t>().ok())
|
||||||
{
|
{
|
||||||
// SAFETY: cleanup of the descendant this test started in its own session.
|
kill_for_cleanup(pid);
|
||||||
unsafe {
|
|
||||||
libc::kill(pid, libc::SIGKILL);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let error = result.expect_err("output still held open must not read as complete");
|
let error = result.expect_err("output still held open must not read as complete");
|
||||||
@@ -950,8 +955,7 @@ fn normal_broker_shutdown_releases_successful_provider_descendant() {
|
|||||||
.trim()
|
.trim()
|
||||||
.parse::<i32>()
|
.parse::<i32>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// SAFETY: signal zero only checks the test-owned provider.
|
assert!(process_exists(provider_pid));
|
||||||
assert_eq!(unsafe { libc::kill(provider_pid, 0) }, 0);
|
|
||||||
|
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
@@ -984,28 +988,19 @@ fn shutdown_channel_peer_loss_kills_retained_provider() {
|
|||||||
.trim()
|
.trim()
|
||||||
.parse::<i32>()
|
.parse::<i32>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// SAFETY: signal zero only checks the test-owned provider.
|
assert!(process_exists(provider_pid));
|
||||||
assert_eq!(unsafe { libc::kill(provider_pid, 0) }, 0);
|
|
||||||
|
|
||||||
// Simulate abrupt parent loss without writing the graceful-shutdown packet.
|
// Simulate abrupt parent loss without writing the graceful-shutdown packet.
|
||||||
assert_eq!(
|
net::shutdown(&guard.broker().inner.shutdown, Shutdown::Both).unwrap();
|
||||||
// SAFETY: the descriptor belongs to this test's live broker guard.
|
|
||||||
unsafe { libc::shutdown(guard.broker().inner.shutdown.as_raw_fd(), libc::SHUT_RDWR) },
|
|
||||||
0
|
|
||||||
);
|
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
let deadline = Instant::now() + Duration::from_secs(1);
|
let deadline = Instant::now() + Duration::from_secs(1);
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: signal zero only probes the recorded test provider PID.
|
if !process_exists(provider_pid) {
|
||||||
if unsafe { libc::kill(provider_pid, 0) } != 0 {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if Instant::now() >= deadline {
|
if Instant::now() >= deadline {
|
||||||
// SAFETY: cleanup keeps a failing regression test from leaking its provider.
|
kill_for_cleanup(provider_pid);
|
||||||
unsafe {
|
|
||||||
libc::kill(provider_pid, libc::SIGKILL);
|
|
||||||
}
|
|
||||||
panic!("provider survived abnormal broker channel loss");
|
panic!("provider survived abnormal broker channel loss");
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(5));
|
thread::sleep(Duration::from_millis(5));
|
||||||
@@ -1069,22 +1064,17 @@ fn retained_publication_replacement_disposes_the_previous_provider() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let deadline = Instant::now() + Duration::from_secs(1);
|
let deadline = Instant::now() + Duration::from_secs(1);
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: signal zero only probes the recorded test provider PID.
|
if !process_exists(first_pid) {
|
||||||
if unsafe { libc::kill(first_pid, 0) } != 0 {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if Instant::now() >= deadline {
|
if Instant::now() >= deadline {
|
||||||
// SAFETY: cleanup keeps a failing regression test from leaking its helpers.
|
kill_for_cleanup(first_pid);
|
||||||
unsafe {
|
kill_for_cleanup(second_pid);
|
||||||
libc::kill(first_pid, libc::SIGKILL);
|
|
||||||
libc::kill(second_pid, libc::SIGKILL);
|
|
||||||
}
|
|
||||||
panic!("replaced publication provider remained alive");
|
panic!("replaced publication provider remained alive");
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(5));
|
thread::sleep(Duration::from_millis(5));
|
||||||
}
|
}
|
||||||
// SAFETY: signal zero only checks the current test-owned provider.
|
assert!(process_exists(second_pid));
|
||||||
assert_eq!(unsafe { libc::kill(second_pid, 0) }, 0);
|
|
||||||
|
|
||||||
drop(guard);
|
drop(guard);
|
||||||
release_test_provider(&second_release_path, &second_proof_path, second_pid);
|
release_test_provider(&second_release_path, &second_proof_path, second_pid);
|
||||||
@@ -1146,20 +1136,15 @@ fn failed_publication_replacement_preserves_the_current_provider() {
|
|||||||
.trim()
|
.trim()
|
||||||
.parse::<i32>()
|
.parse::<i32>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// SAFETY: signal zero only checks the current test-owned provider.
|
assert!(process_exists(current_pid));
|
||||||
assert_eq!(unsafe { libc::kill(current_pid, 0) }, 0);
|
|
||||||
let deadline = Instant::now() + Duration::from_secs(1);
|
let deadline = Instant::now() + Duration::from_secs(1);
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: signal zero only probes the failed test provider PID.
|
if !process_exists(failed_pid) {
|
||||||
if unsafe { libc::kill(failed_pid, 0) } != 0 {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if Instant::now() >= deadline {
|
if Instant::now() >= deadline {
|
||||||
// SAFETY: cleanup keeps a failing regression test from leaking its helpers.
|
kill_for_cleanup(current_pid);
|
||||||
unsafe {
|
kill_for_cleanup(failed_pid);
|
||||||
libc::kill(current_pid, libc::SIGKILL);
|
|
||||||
libc::kill(failed_pid, libc::SIGKILL);
|
|
||||||
}
|
|
||||||
panic!("failed replacement provider survived cleanup");
|
panic!("failed replacement provider survived cleanup");
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(5));
|
thread::sleep(Duration::from_millis(5));
|
||||||
@@ -1205,8 +1190,7 @@ fn retained_publication_kills_failed_or_input_stalled_provider_groups() {
|
|||||||
}
|
}
|
||||||
let deadline = Instant::now() + Duration::from_secs(1);
|
let deadline = Instant::now() + Duration::from_secs(1);
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: signal zero only probes the recorded test child PID.
|
if !process_exists(provider_pid) {
|
||||||
if unsafe { libc::kill(provider_pid, 0) } != 0 {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1307,8 +1291,7 @@ fn broker_shutdown_preempts_retained_publication_stdin_writer() {
|
|||||||
release_test_provider(¤t_release_path, ¤t_proof_path, current_pid);
|
release_test_provider(¤t_release_path, ¤t_proof_path, current_pid);
|
||||||
let deadline = Instant::now() + Duration::from_secs(1);
|
let deadline = Instant::now() + Duration::from_secs(1);
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: signal zero only probes the recorded test child PID.
|
if !process_exists(provider_pid) {
|
||||||
if unsafe { libc::kill(provider_pid, 0) } != 0 {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
+130
-268
@@ -1,10 +1,18 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
use std::io::{self, IoSlice, IoSliceMut};
|
||||||
use std::mem::MaybeUninit;
|
use std::mem::MaybeUninit;
|
||||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
use std::os::fd::{BorrowedFd, OwnedFd};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{io, mem, ptr};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use rustix::event::{self, PollFd, PollFlags, Timespec};
|
||||||
|
use rustix::fs::{self as rustix_fs, FileType, MemfdFlags};
|
||||||
|
use rustix::io::{self as rustix_io, Errno};
|
||||||
|
use rustix::net::sockopt::{self, Timeout};
|
||||||
|
use rustix::net::{
|
||||||
|
self, RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, ReturnFlags, SendAncillaryBuffer,
|
||||||
|
SendAncillaryMessage, SendFlags,
|
||||||
|
};
|
||||||
|
|
||||||
use super::wire::{
|
use super::wire::{
|
||||||
BlobWire, INLINE_BLOB_BYTES, MAX_OUTPUT_BYTES, MAX_PACKET_BYTES, MAX_PACKET_DESCRIPTORS,
|
BlobWire, INLINE_BLOB_BYTES, MAX_OUTPUT_BYTES, MAX_PACKET_BYTES, MAX_PACKET_DESCRIPTORS,
|
||||||
@@ -13,80 +21,47 @@ use super::wire::{
|
|||||||
|
|
||||||
pub(super) const GRACEFUL_SHUTDOWN_BYTE: u8 = 1;
|
pub(super) const GRACEFUL_SHUTDOWN_BYTE: u8 = 1;
|
||||||
|
|
||||||
pub(super) fn set_socket_timeout(fd: RawFd, timeout: Duration) -> io::Result<()> {
|
pub(super) fn set_socket_timeout(fd: BorrowedFd<'_>, timeout: Duration) -> io::Result<()> {
|
||||||
let timeout = timeout.max(Duration::from_millis(1));
|
let timeout = timeout.max(Duration::from_millis(1));
|
||||||
let value = libc::timeval {
|
for option in [Timeout::Send, Timeout::Recv] {
|
||||||
tv_sec: libc::time_t::try_from(timeout.as_secs())
|
sockopt::set_socket_timeout(fd, option, Some(timeout))?;
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "socket timeout overflow"))?,
|
|
||||||
tv_usec: libc::suseconds_t::from(timeout.subsec_micros()),
|
|
||||||
};
|
|
||||||
for option in [libc::SO_SNDTIMEO, libc::SO_RCVTIMEO] {
|
|
||||||
// SAFETY: value is initialized and setsockopt copies it.
|
|
||||||
if unsafe {
|
|
||||||
libc::setsockopt(
|
|
||||||
fd,
|
|
||||||
libc::SOL_SOCKET,
|
|
||||||
option,
|
|
||||||
(&raw const value).cast(),
|
|
||||||
mem::size_of::<libc::timeval>() as libc::socklen_t,
|
|
||||||
)
|
|
||||||
} != 0
|
|
||||||
{
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn shutdown_requested(descriptor: RawFd) -> io::Result<bool> {
|
pub(super) fn shutdown_requested(descriptor: BorrowedFd<'_>) -> io::Result<bool> {
|
||||||
let mut pollfd = libc::pollfd {
|
let mut pollfds = [PollFd::from_borrowed_fd(descriptor, PollFlags::IN)];
|
||||||
fd: descriptor,
|
event::poll(&mut pollfds, Some(&Timespec::default()))?;
|
||||||
events: libc::POLLIN,
|
let revents = pollfds[0].revents();
|
||||||
revents: 0,
|
if revents.contains(PollFlags::NVAL) {
|
||||||
};
|
|
||||||
// SAFETY: pollfd points to one initialized entry.
|
|
||||||
let result = unsafe { libc::poll(&raw mut pollfd, 1, 0) };
|
|
||||||
if result < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
if pollfd.revents & libc::POLLNVAL != 0 {
|
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::BrokenPipe,
|
io::ErrorKind::BrokenPipe,
|
||||||
"broker shutdown channel became invalid",
|
"broker shutdown channel became invalid",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(pollfd.revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR) != 0)
|
Ok(revents.intersects(PollFlags::IN | PollFlags::HUP | PollFlags::ERR))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn take_graceful_shutdown_signal(descriptor: RawFd) -> io::Result<bool> {
|
pub(super) fn take_graceful_shutdown_signal(descriptor: BorrowedFd<'_>) -> io::Result<bool> {
|
||||||
let mut pollfd = libc::pollfd {
|
let mut pollfds = [PollFd::from_borrowed_fd(descriptor, PollFlags::IN)];
|
||||||
fd: descriptor,
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
loop {
|
loop {
|
||||||
// SAFETY: pollfd points to one initialized entry.
|
match event::poll(&mut pollfds, Some(&Timespec::default())) {
|
||||||
let result = unsafe { libc::poll(&raw mut pollfd, 1, 0) };
|
Ok(0) => return Ok(false),
|
||||||
if result < 0 {
|
Ok(_) => {}
|
||||||
let error = io::Error::last_os_error();
|
Err(Errno::INTR) => continue,
|
||||||
if error.kind() == io::ErrorKind::Interrupted {
|
Err(errno) => return Err(errno.into()),
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Err(error);
|
|
||||||
}
|
}
|
||||||
if result == 0 {
|
let revents = pollfds[0].revents();
|
||||||
return Ok(false);
|
if revents.contains(PollFlags::NVAL) {
|
||||||
}
|
|
||||||
if pollfd.revents & libc::POLLNVAL != 0 {
|
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::BrokenPipe,
|
io::ErrorKind::BrokenPipe,
|
||||||
"broker shutdown channel became invalid",
|
"broker shutdown channel became invalid",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if pollfd.revents & libc::POLLIN != 0 {
|
if revents.contains(PollFlags::IN) {
|
||||||
return receive_graceful_shutdown_signal(descriptor);
|
return receive_graceful_shutdown_signal(descriptor);
|
||||||
}
|
}
|
||||||
if pollfd.revents & (libc::POLLHUP | libc::POLLERR) != 0 {
|
if revents.intersects(PollFlags::HUP | PollFlags::ERR) {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::BrokenPipe,
|
io::ErrorKind::BrokenPipe,
|
||||||
"broker shutdown channel closed without a graceful signal",
|
"broker shutdown channel closed without a graceful signal",
|
||||||
@@ -96,27 +71,17 @@ pub(super) fn take_graceful_shutdown_signal(descriptor: RawFd) -> io::Result<boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn receive_graceful_shutdown_signal(descriptor: RawFd) -> io::Result<bool> {
|
fn receive_graceful_shutdown_signal(descriptor: BorrowedFd<'_>) -> io::Result<bool> {
|
||||||
let mut byte = 0_u8;
|
let mut byte = [0_u8];
|
||||||
loop {
|
loop {
|
||||||
// MSG_TRUNC makes an oversized SOCK_SEQPACKET message report its full length.
|
// TRUNC makes an oversized SOCK_SEQPACKET message report its full length.
|
||||||
// SAFETY: byte is a valid one-byte destination for this live broker socket.
|
let flags = RecvFlags::DONTWAIT | RecvFlags::TRUNC;
|
||||||
let received = unsafe {
|
let received = match net::recv(descriptor, &mut byte, flags) {
|
||||||
libc::recv(
|
Ok((_, received)) => received,
|
||||||
descriptor,
|
Err(Errno::INTR) => continue,
|
||||||
(&raw mut byte).cast(),
|
Err(errno) => return Err(errno.into()),
|
||||||
1,
|
|
||||||
libc::MSG_DONTWAIT | libc::MSG_TRUNC,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
if received < 0 {
|
if received == 1 && byte[0] == GRACEFUL_SHUTDOWN_BYTE {
|
||||||
let error = io::Error::last_os_error();
|
|
||||||
if error.kind() == io::ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
if received == 1 && byte == GRACEFUL_SHUTDOWN_BYTE {
|
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
@@ -166,28 +131,11 @@ pub(super) fn decode_blob(
|
|||||||
let mut bytes = vec![0_u8; length];
|
let mut bytes = vec![0_u8; length];
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
while offset < bytes.len() {
|
while offset < bytes.len() {
|
||||||
// SAFETY: the destination range is writable and the validated
|
match rustix_io::pread(&descriptor, &mut bytes[offset..], offset as u64) {
|
||||||
// memfd has exactly the declared immutable length.
|
Ok(0) => bail!("broker memfd ended before its declared length"),
|
||||||
let read = unsafe {
|
Ok(read) => offset += read,
|
||||||
libc::pread(
|
Err(Errno::INTR) => {}
|
||||||
descriptor.as_raw_fd(),
|
Err(errno) => return Err(io::Error::from(errno).into()),
|
||||||
bytes[offset..].as_mut_ptr().cast(),
|
|
||||||
bytes.len() - offset,
|
|
||||||
offset as libc::off_t,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if read > 0 {
|
|
||||||
offset += read as usize;
|
|
||||||
} else if read < 0
|
|
||||||
&& io::Error::last_os_error().kind() == io::ErrorKind::Interrupted
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
return Err(if read == 0 {
|
|
||||||
anyhow!("broker memfd ended before its declared length")
|
|
||||||
} else {
|
|
||||||
io::Error::last_os_error().into()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
@@ -196,206 +144,120 @@ pub(super) fn decode_blob(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sealed_memfd(bytes: &[u8]) -> Result<OwnedFd> {
|
fn sealed_memfd(bytes: &[u8]) -> Result<OwnedFd> {
|
||||||
let name = c"wayscriber-broker-payload";
|
let descriptor = rustix_fs::memfd_create(
|
||||||
// SAFETY: name is a valid C string and flags request a private, sealable fd.
|
c"wayscriber-broker-payload",
|
||||||
let raw =
|
MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING,
|
||||||
unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING) };
|
)
|
||||||
if raw < 0 {
|
.map_err(io::Error::from)
|
||||||
return Err(io::Error::last_os_error()).context("broker memfd creation failed");
|
.context("broker memfd creation failed")?;
|
||||||
}
|
|
||||||
// SAFETY: memfd_create returned a new owned descriptor.
|
|
||||||
let descriptor = unsafe { OwnedFd::from_raw_fd(raw) };
|
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
while offset < bytes.len() {
|
while offset < bytes.len() {
|
||||||
// SAFETY: the source range is readable for the requested length.
|
match rustix_io::write(&descriptor, &bytes[offset..]) {
|
||||||
let written = unsafe {
|
Ok(0) => bail!("short write while creating broker memfd"),
|
||||||
libc::write(
|
Ok(written) => offset += written,
|
||||||
descriptor.as_raw_fd(),
|
Err(Errno::INTR) => {}
|
||||||
bytes[offset..].as_ptr().cast(),
|
Err(errno) => return Err(io::Error::from(errno).into()),
|
||||||
bytes.len() - offset,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if written > 0 {
|
|
||||||
offset += written as usize;
|
|
||||||
} else if written < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
return Err(if written == 0 {
|
|
||||||
anyhow!("short write while creating broker memfd")
|
|
||||||
} else {
|
|
||||||
io::Error::last_os_error().into()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SAFETY: fcntl changes only seal metadata on the owned memfd.
|
rustix_fs::fcntl_add_seals(&descriptor, REQUIRED_MEMFD_SEALS)
|
||||||
if unsafe {
|
.map_err(io::Error::from)
|
||||||
libc::fcntl(
|
.context("failed to seal broker memfd")?;
|
||||||
descriptor.as_raw_fd(),
|
|
||||||
libc::F_ADD_SEALS,
|
|
||||||
REQUIRED_MEMFD_SEALS,
|
|
||||||
)
|
|
||||||
} < 0
|
|
||||||
{
|
|
||||||
return Err(io::Error::last_os_error()).context("failed to seal broker memfd");
|
|
||||||
}
|
|
||||||
Ok(descriptor)
|
Ok(descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_sealed_memfd(descriptor: &OwnedFd, length: usize) -> Result<()> {
|
fn validate_sealed_memfd(descriptor: &OwnedFd, length: usize) -> Result<()> {
|
||||||
let mut stat = MaybeUninit::<libc::stat>::uninit();
|
let stat = rustix_fs::fstat(descriptor)
|
||||||
// SAFETY: stat points to writable storage for fstat.
|
.map_err(io::Error::from)
|
||||||
if unsafe { libc::fstat(descriptor.as_raw_fd(), stat.as_mut_ptr()) } != 0 {
|
.context("failed to inspect broker memfd")?;
|
||||||
return Err(io::Error::last_os_error()).context("failed to inspect broker memfd");
|
if FileType::from_raw_mode(stat.st_mode) != FileType::RegularFile
|
||||||
}
|
|| u64::try_from(stat.st_size).ok() != Some(length as u64)
|
||||||
// SAFETY: fstat initialized stat after success.
|
{
|
||||||
let stat = unsafe { stat.assume_init() };
|
|
||||||
if (stat.st_mode & libc::S_IFMT) != libc::S_IFREG || stat.st_size != length as libc::off_t {
|
|
||||||
bail!("broker memfd shape does not match its declaration");
|
bail!("broker memfd shape does not match its declaration");
|
||||||
}
|
}
|
||||||
// SAFETY: F_GET_SEALS reads descriptor metadata.
|
let sealed = rustix_fs::fcntl_get_seals(descriptor)
|
||||||
let seals = unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_GET_SEALS) };
|
.is_ok_and(|seals| seals.contains(REQUIRED_MEMFD_SEALS));
|
||||||
if seals < 0 || seals & REQUIRED_MEMFD_SEALS != REQUIRED_MEMFD_SEALS {
|
if !sealed {
|
||||||
bail!("broker memfd is not immutably sealed");
|
bail!("broker memfd is not immutably sealed");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn send_packet(fd: RawFd, packet: &[u8], descriptors: &[RawFd]) -> io::Result<()> {
|
pub(super) fn send_packet(
|
||||||
|
fd: BorrowedFd<'_>,
|
||||||
|
packet: &[u8],
|
||||||
|
descriptors: &[BorrowedFd<'_>],
|
||||||
|
) -> io::Result<()> {
|
||||||
if descriptors.len() > MAX_PACKET_DESCRIPTORS {
|
if descriptors.len() > MAX_PACKET_DESCRIPTORS {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
"too many broker packet descriptors",
|
"too many broker packet descriptors",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let mut space = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(MAX_PACKET_DESCRIPTORS))];
|
||||||
|
let mut control = SendAncillaryBuffer::new(&mut space);
|
||||||
|
if !descriptors.is_empty() && !control.push(SendAncillaryMessage::ScmRights(descriptors)) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"broker packet descriptors do not fit their control buffer",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let payload = [IoSlice::new(packet)];
|
||||||
loop {
|
loop {
|
||||||
let mut iovec = libc::iovec {
|
match net::sendmsg(fd, &payload, &mut control, SendFlags::NOSIGNAL) {
|
||||||
iov_base: packet.as_ptr().cast_mut().cast(),
|
Ok(sent) if sent == packet.len() => return Ok(()),
|
||||||
iov_len: packet.len(),
|
Ok(_) => {
|
||||||
};
|
return Err(io::Error::new(
|
||||||
let control_len = if descriptors.is_empty() {
|
io::ErrorKind::WriteZero,
|
||||||
0
|
"short seqpacket send",
|
||||||
} else {
|
));
|
||||||
// SAFETY: CMSG_SPACE only performs arithmetic on its argument.
|
|
||||||
unsafe { libc::CMSG_SPACE(mem::size_of_val(descriptors) as libc::c_uint) as usize }
|
|
||||||
};
|
|
||||||
let mut control = vec![0_usize; control_len.div_ceil(mem::size_of::<usize>())];
|
|
||||||
// SAFETY: zero is a valid initial state for msghdr.
|
|
||||||
let mut message: libc::msghdr = unsafe { mem::zeroed() };
|
|
||||||
message.msg_iov = &raw mut iovec;
|
|
||||||
message.msg_iovlen = 1;
|
|
||||||
if !descriptors.is_empty() {
|
|
||||||
message.msg_control = control.as_mut_ptr().cast();
|
|
||||||
message.msg_controllen = control_len;
|
|
||||||
// SAFETY: the control buffer has CMSG_SPACE bytes.
|
|
||||||
unsafe {
|
|
||||||
let header = libc::CMSG_FIRSTHDR(&raw const message);
|
|
||||||
(*header).cmsg_level = libc::SOL_SOCKET;
|
|
||||||
(*header).cmsg_type = libc::SCM_RIGHTS;
|
|
||||||
(*header).cmsg_len =
|
|
||||||
libc::CMSG_LEN(mem::size_of_val(descriptors) as libc::c_uint) as usize;
|
|
||||||
ptr::copy_nonoverlapping(
|
|
||||||
descriptors.as_ptr(),
|
|
||||||
libc::CMSG_DATA(header).cast::<RawFd>(),
|
|
||||||
descriptors.len(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
Err(Errno::INTR) => {}
|
||||||
|
Err(errno) => return Err(errno.into()),
|
||||||
}
|
}
|
||||||
// SAFETY: message references live packet/control buffers.
|
|
||||||
let sent = unsafe { libc::sendmsg(fd, &raw const message, libc::MSG_NOSIGNAL) };
|
|
||||||
if sent == packet.len() as isize {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if sent < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Err(if sent < 0 {
|
|
||||||
io::Error::last_os_error()
|
|
||||||
} else {
|
|
||||||
io::Error::new(io::ErrorKind::WriteZero, "short seqpacket send")
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn recv_packet(fd: RawFd) -> io::Result<(Vec<u8>, Vec<OwnedFd>)> {
|
pub(super) fn recv_packet(fd: BorrowedFd<'_>) -> io::Result<(Vec<u8>, Vec<OwnedFd>)> {
|
||||||
let mut buffer = vec![0_u8; MAX_PACKET_BYTES + 1];
|
let mut buffer = vec![0_u8; MAX_PACKET_BYTES + 1];
|
||||||
loop {
|
let mut space = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(MAX_PACKET_DESCRIPTORS))];
|
||||||
let mut iovec = libc::iovec {
|
let mut control = RecvAncillaryBuffer::new(&mut space);
|
||||||
iov_base: buffer.as_mut_ptr().cast(),
|
let received = loop {
|
||||||
iov_len: buffer.len(),
|
let mut payload = [IoSliceMut::new(&mut buffer)];
|
||||||
};
|
match net::recvmsg(fd, &mut payload, &mut control, RecvFlags::CMSG_CLOEXEC) {
|
||||||
// SAFETY: CMSG_SPACE only performs arithmetic on its argument.
|
Ok(received) => break received,
|
||||||
let control_len = unsafe {
|
Err(Errno::INTR) => {}
|
||||||
libc::CMSG_SPACE((MAX_PACKET_DESCRIPTORS * mem::size_of::<RawFd>()) as libc::c_uint)
|
Err(errno) => return Err(errno.into()),
|
||||||
as usize
|
|
||||||
};
|
|
||||||
let mut control = vec![0_usize; control_len.div_ceil(mem::size_of::<usize>())];
|
|
||||||
// SAFETY: zero is a valid initial state for msghdr.
|
|
||||||
let mut message: libc::msghdr = unsafe { mem::zeroed() };
|
|
||||||
message.msg_iov = &raw mut iovec;
|
|
||||||
message.msg_iovlen = 1;
|
|
||||||
message.msg_control = control.as_mut_ptr().cast();
|
|
||||||
message.msg_controllen = control_len;
|
|
||||||
// SAFETY: message references writable packet/control buffers.
|
|
||||||
let read = unsafe { libc::recvmsg(fd, &raw mut message, libc::MSG_CMSG_CLOEXEC) };
|
|
||||||
if read > 0 {
|
|
||||||
let read = read as usize;
|
|
||||||
let mut descriptors = Vec::new();
|
|
||||||
// SAFETY: recvmsg initialized ancillary headers in the buffer.
|
|
||||||
unsafe {
|
|
||||||
let mut header = libc::CMSG_FIRSTHDR(&raw const message);
|
|
||||||
while !header.is_null() {
|
|
||||||
if (*header).cmsg_level != libc::SOL_SOCKET
|
|
||||||
|| (*header).cmsg_type != libc::SCM_RIGHTS
|
|
||||||
{
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"unexpected broker packet ancillary data",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let base_len = libc::CMSG_LEN(0) as usize;
|
|
||||||
if (*header).cmsg_len < base_len {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"malformed broker descriptor payload",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let data_len = (*header).cmsg_len - base_len;
|
|
||||||
if !data_len.is_multiple_of(mem::size_of::<RawFd>()) {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"misaligned broker descriptor payload",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let count = data_len / mem::size_of::<RawFd>();
|
|
||||||
let data = libc::CMSG_DATA(header).cast::<RawFd>();
|
|
||||||
for index in 0..count {
|
|
||||||
descriptors.push(OwnedFd::from_raw_fd(*data.add(index)));
|
|
||||||
}
|
|
||||||
header = libc::CMSG_NXTHDR(&raw const message, header);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if read > MAX_PACKET_BYTES
|
|
||||||
|| message.msg_flags & (libc::MSG_TRUNC | libc::MSG_CTRUNC) != 0
|
|
||||||
|| descriptors.len() > MAX_PACKET_DESCRIPTORS
|
|
||||||
{
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"broker packet exceeds its data or descriptor cap",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
buffer.truncate(read);
|
|
||||||
return Ok((buffer, descriptors));
|
|
||||||
}
|
}
|
||||||
if read == 0 {
|
};
|
||||||
return Err(io::Error::new(
|
if received.bytes == 0 {
|
||||||
io::ErrorKind::UnexpectedEof,
|
return Err(io::Error::new(
|
||||||
"broker socket closed",
|
io::ErrorKind::UnexpectedEof,
|
||||||
));
|
"broker socket closed",
|
||||||
}
|
));
|
||||||
let error = io::Error::last_os_error();
|
|
||||||
if error.kind() == io::ErrorKind::Interrupted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Err(error);
|
|
||||||
}
|
}
|
||||||
|
// Descriptors left in `control` close when it drops, so every rejection
|
||||||
|
// below releases what the packet carried.
|
||||||
|
let mut descriptors = Vec::new();
|
||||||
|
for message in control.drain() {
|
||||||
|
let RecvAncillaryMessage::ScmRights(passed) = message else {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"unexpected broker packet ancillary data",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
descriptors.extend(passed);
|
||||||
|
}
|
||||||
|
if received.bytes > MAX_PACKET_BYTES
|
||||||
|
|| received
|
||||||
|
.flags
|
||||||
|
.intersects(ReturnFlags::TRUNC | ReturnFlags::CTRUNC)
|
||||||
|
|| descriptors.len() > MAX_PACKET_DESCRIPTORS
|
||||||
|
{
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"broker packet exceeds its data or descriptor cap",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
buffer.truncate(received.bytes);
|
||||||
|
Ok((buffer, descriptors))
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ use std::os::fd::OwnedFd;
|
|||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
|
use rustix::fs::SealFlags;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub(super) const BROKER_FD: i32 = 3;
|
pub(super) const BROKER_FD: i32 = 3;
|
||||||
@@ -19,8 +20,10 @@ pub(super) const MAX_OUTPUT_BYTES: usize = 256 * 1024 * 1024;
|
|||||||
pub(super) const MAX_STDERR_BYTES: usize = 64 * 1024;
|
pub(super) const MAX_STDERR_BYTES: usize = 64 * 1024;
|
||||||
pub(super) const MAX_OWNED_CHILDREN: usize = 64;
|
pub(super) const MAX_OWNED_CHILDREN: usize = 64;
|
||||||
pub(super) const MAX_PACKET_DESCRIPTORS: usize = 2;
|
pub(super) const MAX_PACKET_DESCRIPTORS: usize = 2;
|
||||||
pub(super) const REQUIRED_MEMFD_SEALS: i32 =
|
pub(super) const REQUIRED_MEMFD_SEALS: SealFlags = SealFlags::WRITE
|
||||||
libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_SEAL;
|
.union(SealFlags::GROW)
|
||||||
|
.union(SealFlags::SHRINK)
|
||||||
|
.union(SealFlags::SEAL);
|
||||||
|
|
||||||
/// Failure text used when a `Complete`-mode helper writes past its stdout cap.
|
/// Failure text used when a `Complete`-mode helper writes past its stdout cap.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use std::ffi::{CString, OsStr, OsString};
|
use std::ffi::{CString, OsStr, OsString};
|
||||||
use std::fs::{self, File, OpenOptions};
|
use std::fs::{self, File, OpenOptions};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::mem::MaybeUninit;
|
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
|
||||||
use std::os::fd::{AsRawFd, FromRawFd};
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -10,6 +9,11 @@ use std::process;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use rustix::fs::RenameFlags;
|
||||||
|
use rustix::fs::{self as rustix_fs, AtFlags, Mode, OFlags};
|
||||||
|
use rustix::io::Errno;
|
||||||
|
|
||||||
use crate::runtime_ui_state::{RuntimeStateFileIdentity, RuntimeStateResolvedParent};
|
use crate::runtime_ui_state::{RuntimeStateFileIdentity, RuntimeStateResolvedParent};
|
||||||
|
|
||||||
static NEXT_SIBLING_ID: AtomicU64 = AtomicU64::new(1);
|
static NEXT_SIBLING_ID: AtomicU64 = AtomicU64::new(1);
|
||||||
@@ -62,8 +66,8 @@ impl PinnedParent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fd(&self) -> libc::c_int {
|
fn fd(&self) -> BorrowedFd<'_> {
|
||||||
self.inner.file.as_raw_fd()
|
self.inner.file.as_fd()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync(&self) -> io::Result<()> {
|
fn sync(&self) -> io::Result<()> {
|
||||||
@@ -77,7 +81,7 @@ impl PinnedParent {
|
|||||||
return Ok(self.inner.observed_path.clone());
|
return Ok(self.inner.observed_path.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let proc_path = PathBuf::from(format!("/proc/self/fd/{}", self.fd()));
|
let proc_path = PathBuf::from(format!("/proc/self/fd/{}", self.fd().as_raw_fd()));
|
||||||
let current = fs::read_link(proc_path)?;
|
let current = fs::read_link(proc_path)?;
|
||||||
if !fs::metadata(¤t)
|
if !fs::metadata(¤t)
|
||||||
.is_ok_and(|metadata| file_identity(&metadata) == self.inner.identity)
|
.is_ok_and(|metadata| file_identity(&metadata) == self.inner.identity)
|
||||||
@@ -100,15 +104,13 @@ pub(super) struct PinnedPath {
|
|||||||
impl PinnedPath {
|
impl PinnedPath {
|
||||||
pub(super) fn open_read(&self) -> io::Result<File> {
|
pub(super) fn open_read(&self) -> io::Result<File> {
|
||||||
let leaf = os_str_c_string(&self.leaf)?;
|
let leaf = os_str_c_string(&self.leaf)?;
|
||||||
// SAFETY: the parent fd and CString remain valid for the duration of the call.
|
let fd = rustix_fs::openat(
|
||||||
let fd = unsafe {
|
self.parent.fd(),
|
||||||
libc::openat(
|
&leaf,
|
||||||
self.parent.fd(),
|
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
|
||||||
leaf.as_ptr(),
|
Mode::empty(),
|
||||||
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK,
|
)?;
|
||||||
)
|
Ok(File::from(fd))
|
||||||
};
|
|
||||||
file_from_fd(fd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn reported_path(&self) -> io::Result<PathBuf> {
|
pub(super) fn reported_path(&self) -> io::Result<PathBuf> {
|
||||||
@@ -119,40 +121,23 @@ impl PinnedPath {
|
|||||||
self.parent.sync()
|
self.parent.sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_new(&self, mode: libc::mode_t) -> io::Result<File> {
|
fn create_new(&self, mode: Mode) -> io::Result<File> {
|
||||||
let leaf = os_str_c_string(&self.leaf)?;
|
let leaf = os_str_c_string(&self.leaf)?;
|
||||||
// SAFETY: the parent fd and CString remain valid for the duration of the call.
|
let fd = rustix_fs::openat(
|
||||||
let fd = unsafe {
|
self.parent.fd(),
|
||||||
libc::openat(
|
&leaf,
|
||||||
self.parent.fd(),
|
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
leaf.as_ptr(),
|
mode,
|
||||||
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC | libc::O_NOFOLLOW,
|
)?;
|
||||||
mode,
|
Ok(File::from(fd))
|
||||||
)
|
|
||||||
};
|
|
||||||
file_from_fd(fd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exists_nofollow(&self) -> io::Result<bool> {
|
fn exists_nofollow(&self) -> io::Result<bool> {
|
||||||
let leaf = os_str_c_string(&self.leaf)?;
|
let leaf = os_str_c_string(&self.leaf)?;
|
||||||
let mut stat = MaybeUninit::<libc::stat>::uninit();
|
match rustix_fs::statat(self.parent.fd(), &leaf, AtFlags::SYMLINK_NOFOLLOW) {
|
||||||
// SAFETY: the pointers are valid and stat points to writable storage.
|
Ok(_) => Ok(true),
|
||||||
let result = unsafe {
|
Err(Errno::NOENT) => Ok(false),
|
||||||
libc::fstatat(
|
Err(errno) => Err(errno.into()),
|
||||||
self.parent.fd(),
|
|
||||||
leaf.as_ptr(),
|
|
||||||
stat.as_mut_ptr(),
|
|
||||||
libc::AT_SYMLINK_NOFOLLOW,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if result == 0 {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
let error = io::Error::last_os_error();
|
|
||||||
if error.kind() == io::ErrorKind::NotFound {
|
|
||||||
Ok(false)
|
|
||||||
} else {
|
|
||||||
Err(error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,18 +169,20 @@ impl Drop for CleanupPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn create_synced_temp(path: &PinnedPath, bytes: &[u8]) -> io::Result<CleanupPath> {
|
pub(super) fn create_synced_temp(path: &PinnedPath, bytes: &[u8]) -> io::Result<CleanupPath> {
|
||||||
let path = create_unique_sibling(path, "tmp", |candidate| match candidate.create_new(0o600) {
|
let path = create_unique_sibling(path, "tmp", |candidate| {
|
||||||
Ok(mut file) => {
|
match candidate.create_new(Mode::RUSR | Mode::WUSR) {
|
||||||
let result = file.write_all(bytes).and_then(|()| file.sync_all());
|
Ok(mut file) => {
|
||||||
drop(file);
|
let result = file.write_all(bytes).and_then(|()| file.sync_all());
|
||||||
if let Err(error) = result {
|
drop(file);
|
||||||
let _ = remove_file(candidate);
|
if let Err(error) = result {
|
||||||
return Err(error);
|
let _ = remove_file(candidate);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok(true)
|
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(false),
|
||||||
|
Err(error) => Err(error),
|
||||||
}
|
}
|
||||||
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(false),
|
|
||||||
Err(error) => Err(error),
|
|
||||||
})?;
|
})?;
|
||||||
Ok(CleanupPath::new(path))
|
Ok(CleanupPath::new(path))
|
||||||
}
|
}
|
||||||
@@ -236,21 +223,14 @@ pub(super) fn rename_noreplace(source: &PinnedPath, destination: &PinnedPath) ->
|
|||||||
{
|
{
|
||||||
let source_leaf = os_str_c_string(&source.leaf)?;
|
let source_leaf = os_str_c_string(&source.leaf)?;
|
||||||
let destination_leaf = os_str_c_string(&destination.leaf)?;
|
let destination_leaf = os_str_c_string(&destination.leaf)?;
|
||||||
// SAFETY: both parent fds and CStrings remain valid for the call.
|
rustix_fs::renameat_with(
|
||||||
let result = unsafe {
|
source.parent.fd(),
|
||||||
libc::renameat2(
|
&source_leaf,
|
||||||
source.parent.fd(),
|
destination.parent.fd(),
|
||||||
source_leaf.as_ptr(),
|
&destination_leaf,
|
||||||
destination.parent.fd(),
|
RenameFlags::NOREPLACE,
|
||||||
destination_leaf.as_ptr(),
|
)
|
||||||
libc::RENAME_NOREPLACE,
|
.map_err(io::Error::from)
|
||||||
)
|
|
||||||
};
|
|
||||||
if result == 0 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(io::Error::last_os_error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
{
|
{
|
||||||
@@ -264,13 +244,7 @@ pub(super) fn rename_noreplace(source: &PinnedPath, destination: &PinnedPath) ->
|
|||||||
|
|
||||||
pub(super) fn remove_file(path: &PinnedPath) -> io::Result<()> {
|
pub(super) fn remove_file(path: &PinnedPath) -> io::Result<()> {
|
||||||
let leaf = os_str_c_string(&path.leaf)?;
|
let leaf = os_str_c_string(&path.leaf)?;
|
||||||
// SAFETY: the parent fd and CString remain valid for the duration of the call.
|
rustix_fs::unlinkat(path.parent.fd(), &leaf, AtFlags::empty()).map_err(io::Error::from)
|
||||||
let result = unsafe { libc::unlinkat(path.parent.fd(), leaf.as_ptr(), 0) };
|
|
||||||
if result == 0 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(io::Error::last_os_error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_leaf(leaf: &OsStr) -> io::Result<()> {
|
fn validate_leaf(leaf: &OsStr) -> io::Result<()> {
|
||||||
@@ -288,14 +262,6 @@ fn os_str_c_string(value: &OsStr) -> io::Result<CString> {
|
|||||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains a NUL byte"))
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains a NUL byte"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_from_fd(fd: libc::c_int) -> io::Result<File> {
|
|
||||||
if fd < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
// SAFETY: openat returned a new owned descriptor on success.
|
|
||||||
Ok(unsafe { File::from_raw_fd(fd) })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_identity(metadata: &fs::Metadata) -> RuntimeStateFileIdentity {
|
fn file_identity(metadata: &fs::Metadata) -> RuntimeStateFileIdentity {
|
||||||
RuntimeStateFileIdentity {
|
RuntimeStateFileIdentity {
|
||||||
device: metadata.dev(),
|
device: metadata.dev(),
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use std::ffi::CString;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
use std::os::unix::fs::{PermissionsExt, symlink};
|
use std::os::unix::fs::{PermissionsExt, symlink};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -257,9 +255,7 @@ fn target_symlink_is_rejected_and_neither_path_is_modified() {
|
|||||||
fn fifo_is_rejected_without_blocking_for_a_writer() {
|
fn fifo_is_rejected_without_blocking_for_a_writer() {
|
||||||
let temp = test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let path = temp.path().join("runtime-ui.toml");
|
let path = temp.path().join("runtime-ui.toml");
|
||||||
let c_path = CString::new(path.as_os_str().as_bytes()).unwrap();
|
test_temp::make_fifo(&path);
|
||||||
// SAFETY: the CString is NUL-terminated and remains live for the call.
|
|
||||||
assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) }, 0);
|
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
assert!(RuntimeUiStateStore::new(path).inspect().is_err());
|
assert!(RuntimeUiStateStore::new(path).inspect().is_err());
|
||||||
assert!(started.elapsed() < Duration::from_secs(1));
|
assert!(started.elapsed() < Duration::from_secs(1));
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use rustix::fs::{self as rustix_fs, RenameFlags};
|
||||||
use std::ffi::{OsStr, OsString};
|
use std::ffi::{OsStr, OsString};
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -386,23 +388,14 @@ fn rename_artifacts_with_rollback(artifacts: &[MoveArtifact]) -> Result<()> {
|
|||||||
pub(crate) fn rename_artifact_no_replace(source: &Path, target: &Path) -> io::Result<()> {
|
pub(crate) fn rename_artifact_no_replace(source: &Path, target: &Path) -> io::Result<()> {
|
||||||
let source = path_to_cstring(source)?;
|
let source = path_to_cstring(source)?;
|
||||||
let target = path_to_cstring(target)?;
|
let target = path_to_cstring(target)?;
|
||||||
// SAFETY: The C strings are valid, NUL-terminated paths. AT_FDCWD makes both paths
|
rustix_fs::renameat_with(
|
||||||
// relative to the current process, matching std::fs::rename path semantics.
|
rustix_fs::CWD,
|
||||||
let result = unsafe {
|
&source,
|
||||||
libc::syscall(
|
rustix_fs::CWD,
|
||||||
libc::SYS_renameat2,
|
&target,
|
||||||
libc::AT_FDCWD,
|
RenameFlags::NOREPLACE,
|
||||||
source.as_ptr(),
|
)
|
||||||
libc::AT_FDCWD,
|
.map_err(io::Error::from)
|
||||||
target.as_ptr(),
|
|
||||||
libc::RENAME_NOREPLACE,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if result == 0 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(io::Error::last_os_error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::ffi::CString;
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::fs::{PermissionsExt, symlink};
|
use std::os::unix::fs::{PermissionsExt, symlink};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::{fs, io, process};
|
use std::{fs, process};
|
||||||
|
|
||||||
use crate::{test_env, test_temp};
|
use crate::{test_env, test_temp};
|
||||||
|
|
||||||
@@ -250,7 +246,7 @@ fn named_file_validation_rejects_missing_directory_shaped_path() {
|
|||||||
fn named_file_validation_rejects_fifo_target() {
|
fn named_file_validation_rejects_fifo_target() {
|
||||||
let temp = test_temp::tempdir().unwrap();
|
let temp = test_temp::tempdir().unwrap();
|
||||||
let path = temp.path().join("session-fifo.wayscriber-session");
|
let path = temp.path().join("session-fifo.wayscriber-session");
|
||||||
make_fifo(&path);
|
test_temp::make_fifo(&path);
|
||||||
|
|
||||||
for (label, result) in [
|
for (label, result) in [
|
||||||
(
|
(
|
||||||
@@ -394,17 +390,3 @@ fn can_create_probe(parent: &Path) -> bool {
|
|||||||
Err(_) => false,
|
Err(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn make_fifo(path: &Path) {
|
|
||||||
let raw_path = CString::new(path.as_os_str().as_bytes()).expect("fifo path has no NUL bytes");
|
|
||||||
// SAFETY: raw_path is a valid, NUL-terminated filesystem path for this process.
|
|
||||||
let result = unsafe { libc::mkfifo(raw_path.as_ptr(), 0o600) };
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
0,
|
|
||||||
"mkfifo {} failed: {}",
|
|
||||||
path.display(),
|
|
||||||
io::Error::last_os_error()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ use super::*;
|
|||||||
use crate::session::snapshot::tests::sample_snapshot;
|
use crate::session::snapshot::tests::sample_snapshot;
|
||||||
use crate::session::snapshot::{load, save};
|
use crate::session::snapshot::{load, save};
|
||||||
use crate::session::{CompressionMode, SessionOptions};
|
use crate::session::{CompressionMode, SessionOptions};
|
||||||
use crate::test_temp::tempdir;
|
use crate::test_temp::{self, tempdir};
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{Seek, Write};
|
use std::io::{Seek, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -427,7 +427,7 @@ fn save_as_and_gzip_preserve_the_allocated_header_generation() {
|
|||||||
fn fifo_marker_probe_never_blocks_and_oversized_generations_do_not_raise_the_sequence() {
|
fn fifo_marker_probe_never_blocks_and_oversized_generations_do_not_raise_the_sequence() {
|
||||||
let temp = tempdir().unwrap();
|
let temp = tempdir().unwrap();
|
||||||
let options = options(temp.path());
|
let options = options(temp.path());
|
||||||
super::super::tests::make_fifo(&options.clear_marker_file_path());
|
test_temp::make_fifo(&options.clear_marker_file_path());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ArtifactSetView::probe(&options)
|
ArtifactSetView::probe(&options)
|
||||||
.next_generation(UnreadablePolicy::Refuse)
|
.next_generation(UnreadablePolicy::Refuse)
|
||||||
|
|||||||
@@ -22,19 +22,15 @@ use crate::draw::frame::{MAX_COMPOUND_DEPTH, ShapeSnapshot, UndoAction};
|
|||||||
use crate::draw::{ArrowStyle, Color, FontDescriptor, Frame, Shape};
|
use crate::draw::{ArrowStyle, Color, FontDescriptor, Frame, Shape};
|
||||||
use crate::input::EraserMode;
|
use crate::input::EraserMode;
|
||||||
use crate::session::options::{CompressionMode, SessionOptions};
|
use crate::session::options::{CompressionMode, SessionOptions};
|
||||||
use crate::test_temp::tempdir;
|
use crate::test_temp::{make_fifo, tempdir};
|
||||||
use crate::time_utils::now_rfc3339;
|
use crate::time_utils::now_rfc3339;
|
||||||
#[cfg(unix)]
|
|
||||||
use std::ffi::CString;
|
|
||||||
use std::fs::{File, Permissions};
|
use std::fs::{File, Permissions};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
use std::{fs, io, thread};
|
use std::{fs, thread};
|
||||||
|
|
||||||
fn sample_frame() -> Frame {
|
fn sample_frame() -> Frame {
|
||||||
let mut frame = Frame::new();
|
let mut frame = Frame::new();
|
||||||
@@ -281,20 +277,6 @@ fn write_contentless_session(path: &Path) {
|
|||||||
.expect("contentless session write");
|
.expect("contentless session write");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub(super) fn make_fifo(path: &Path) {
|
|
||||||
let raw_path = CString::new(path.as_os_str().as_bytes()).expect("fifo path has no NUL bytes");
|
|
||||||
// SAFETY: raw_path is a valid, NUL-terminated filesystem path for this process.
|
|
||||||
let result = unsafe { libc::mkfifo(raw_path.as_ptr(), 0o600) };
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
0,
|
|
||||||
"mkfifo {} failed: {}",
|
|
||||||
path.display(),
|
|
||||||
io::Error::last_os_error()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn set_modified(path: &Path, modified: SystemTime) {
|
pub(super) fn set_modified(path: &Path, modified: SystemTime) {
|
||||||
File::options()
|
File::options()
|
||||||
.write(true)
|
.write(true)
|
||||||
|
|||||||
@@ -8,14 +8,9 @@ use crate::session::snapshot::{BoardPagesSnapshot, BoardSnapshot};
|
|||||||
use crate::session::{
|
use crate::session::{
|
||||||
CompressionMode, SessionOptions, SessionSnapshot, ToolStateSnapshot, save_snapshot,
|
CompressionMode, SessionOptions, SessionSnapshot, ToolStateSnapshot, save_snapshot,
|
||||||
};
|
};
|
||||||
#[cfg(unix)]
|
use std::fs;
|
||||||
use std::ffi::CString;
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
use std::path::Path;
|
|
||||||
use std::{fs, io};
|
|
||||||
|
|
||||||
fn transparent_line_snapshot() -> SessionSnapshot {
|
fn transparent_line_snapshot() -> SessionSnapshot {
|
||||||
let mut frame = Frame::new();
|
let mut frame = Frame::new();
|
||||||
@@ -632,7 +627,7 @@ fn inspect_named_file_rejects_special_lock_without_blocking() {
|
|||||||
save_snapshot(&snapshot, &options).expect("save_snapshot should succeed");
|
save_snapshot(&snapshot, &options).expect("save_snapshot should succeed");
|
||||||
let lock_path = options.lock_file_path();
|
let lock_path = options.lock_file_path();
|
||||||
fs::remove_file(&lock_path).expect("remove named session lock");
|
fs::remove_file(&lock_path).expect("remove named session lock");
|
||||||
make_fifo(&lock_path);
|
test_temp::make_fifo(&lock_path);
|
||||||
|
|
||||||
let err = inspect_session(&options).expect_err("special lock should be rejected");
|
let err = inspect_session(&options).expect_err("special lock should be rejected");
|
||||||
|
|
||||||
@@ -722,17 +717,3 @@ fn clear_named_file_rejects_symlink_primary_without_removing_artifacts() {
|
|||||||
"clear must not remove selected sidecars after primary validation fails"
|
"clear must not remove selected sidecars after primary validation fails"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn make_fifo(path: &Path) {
|
|
||||||
let raw_path = CString::new(path.as_os_str().as_bytes()).expect("fifo path has no NUL bytes");
|
|
||||||
// SAFETY: raw_path is a valid, NUL-terminated filesystem path for this process.
|
|
||||||
let result = unsafe { libc::mkfifo(raw_path.as_ptr(), 0o600) };
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
0,
|
|
||||||
"mkfifo {} failed: {}",
|
|
||||||
path.display(),
|
|
||||||
io::Error::last_os_error()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,8 @@ use std::os::unix::fs as unix_fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::{env, fs, io, process};
|
use std::{env, fs, io, process};
|
||||||
|
|
||||||
|
use rustix::process as rustix_process;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
use crate::durable_io;
|
use crate::durable_io;
|
||||||
@@ -124,8 +126,7 @@ fn play(arguments: &[OsString]) -> Result<()> {
|
|||||||
let report: Vec<u8> = match role {
|
let report: Vec<u8> = match role {
|
||||||
Role::Exit => return Ok(()),
|
Role::Exit => return Ok(()),
|
||||||
Role::ReportProcessGroup => {
|
Role::ReportProcessGroup => {
|
||||||
// SAFETY: getpgrp has no preconditions and cannot fail.
|
let leader = rustix_process::getpgrp().as_raw_pid() == process::id() as libc::pid_t;
|
||||||
let leader = unsafe { libc::getpgrp() } == process::id() as libc::pid_t;
|
|
||||||
if leader {
|
if leader {
|
||||||
"process-group-leader"
|
"process-group-leader"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1 +1,19 @@
|
|||||||
|
#[cfg(unix)]
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use rustix::fs::{self as rustix_fs, FileType, Mode};
|
||||||
pub(crate) use tempfile::{TempDir, tempdir};
|
pub(crate) use tempfile::{TempDir, tempdir};
|
||||||
|
|
||||||
|
/// Creates a FIFO that only the current user can open.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub(crate) fn make_fifo(path: &Path) {
|
||||||
|
rustix_fs::mknodat(
|
||||||
|
rustix_fs::CWD,
|
||||||
|
path,
|
||||||
|
FileType::Fifo,
|
||||||
|
Mode::RUSR | Mode::WUSR,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|errno| panic!("mkfifo {} failed: {errno}", path.display()));
|
||||||
|
}
|
||||||
@@ -527,7 +527,8 @@ mod tests {
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use glib::MainContext;
|
use glib::MainContext;
|
||||||
use std::os::fd::AsRawFd;
|
use rustix::process::Pid;
|
||||||
|
use rustix::thread as rustix_thread;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -570,18 +571,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wake_is_readable(source: &RuntimeWakeSource) -> bool {
|
fn wake_is_readable(source: &RuntimeWakeSource) -> bool {
|
||||||
let mut pollfd = libc::pollfd {
|
source.is_readable_within(Duration::ZERO).unwrap()
|
||||||
fd: source.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: pollfd and the source descriptor are valid for this non-blocking poll.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 0) };
|
|
||||||
assert!(ready >= 0);
|
|
||||||
ready > 0 && pollfd.revents & libc::POLLIN != 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_until_thread_is_blocked_in_poll(tid: libc::pid_t) {
|
fn wait_until_thread_is_blocked_in_poll(tid: Pid) {
|
||||||
let stat_path = format!("/proc/self/task/{tid}/stat");
|
let stat_path = format!("/proc/self/task/{tid}/stat");
|
||||||
let deadline = Instant::now() + Duration::from_secs(1);
|
let deadline = Instant::now() + Duration::from_secs(1);
|
||||||
loop {
|
loop {
|
||||||
@@ -814,18 +807,8 @@ mod tests {
|
|||||||
let (wake, _health, publisher) = channel();
|
let (wake, _health, publisher) = channel();
|
||||||
let (tid_tx, tid_rx) = mpsc::channel();
|
let (tid_tx, tid_rx) = mpsc::channel();
|
||||||
let poller = thread::spawn(move || {
|
let poller = thread::spawn(move || {
|
||||||
// SAFETY: gettid has no preconditions and is used only to observe this test thread.
|
tid_tx.send(rustix_thread::gettid()).unwrap();
|
||||||
let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t };
|
assert!(wake.is_readable_within(Duration::from_secs(1)).unwrap());
|
||||||
tid_tx.send(tid).unwrap();
|
|
||||||
let mut pollfd = libc::pollfd {
|
|
||||||
fd: wake.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: pollfd and the source descriptor remain valid for the bounded wait.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 1_000) };
|
|
||||||
assert_eq!(ready, 1);
|
|
||||||
assert_ne!(pollfd.revents & libc::POLLIN, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let tid = tid_rx.recv().unwrap();
|
let tid = tid_rx.recv().unwrap();
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ pub(super) fn run(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::os::fd::AsRawFd;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::backend::wayland::RuntimeWakeSource;
|
use crate::backend::wayland::RuntimeWakeSource;
|
||||||
@@ -140,16 +140,8 @@ mod tests {
|
|||||||
clean: Cell::new(false),
|
clean: Cell::new(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut pollfd = libc::pollfd {
|
assert!(wake.is_readable_within(Duration::ZERO).unwrap());
|
||||||
fd: wake.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: pollfd and the source descriptor are valid for this non-blocking poll.
|
|
||||||
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 0) };
|
|
||||||
assert_eq!(ready, 1);
|
|
||||||
assert!(health.failed());
|
assert!(health.failed());
|
||||||
assert_ne!(pollfd.revents & libc::POLLIN, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -162,13 +154,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert!(!health.failed());
|
assert!(!health.failed());
|
||||||
let mut pollfd = libc::pollfd {
|
assert!(!wake.is_readable_within(Duration::ZERO).unwrap());
|
||||||
fd: wake.poll_fd().as_raw_fd(),
|
|
||||||
events: libc::POLLIN,
|
|
||||||
revents: 0,
|
|
||||||
};
|
|
||||||
// SAFETY: pollfd and the source descriptor are valid for this non-blocking poll.
|
|
||||||
assert_eq!(unsafe { libc::poll(&raw mut pollfd, 1, 0) }, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
use std::{fmt, io};
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use rustix::thread as rustix_thread;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
SIGNAL_WRITE_FD, begin_listener_teardown, clear_registered_signals, create_pipe,
|
SIGNAL_WRITE_FD, begin_listener_teardown, clear_registered_signals, create_pipe,
|
||||||
dispatch_pending_signals, finish_listener_teardown, install_handler, publish_listener_active,
|
dispatch_pending_signals, finish_listener_teardown, install_handler, publish_listener_active,
|
||||||
@@ -259,9 +262,9 @@ impl Drop for SignalListener {
|
|||||||
fn listener_thread(read_fd: RawFd, shared: &ListenerShared) {
|
fn listener_thread(read_fd: RawFd, shared: &ListenerShared) {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
{
|
{
|
||||||
// SAFETY: gettid has no preconditions and is used only by tests to
|
// Used only by tests to observe that this exact thread reached its
|
||||||
// observe that this exact thread reached its blocking read.
|
// blocking read.
|
||||||
let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t };
|
let tid = rustix_thread::gettid().as_raw_pid();
|
||||||
shared.thread_tid.store(tid, Ordering::Release);
|
shared.thread_tid.store(tid, Ordering::Release);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -343,6 +343,19 @@ fn clustered_help_and_version_exit_zero_without_runtime_startup() {
|
|||||||
.stdout_starts_with("wayscriber ");
|
.stdout_starts_with("wayscriber ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn internal_broker_entry_rejects_descriptors_that_are_not_broker_sockets() {
|
||||||
|
run_command(
|
||||||
|
wayscriber_cmd()
|
||||||
|
.env("WAYSCRIBER_INTERNAL_PROCESS_BROKER_FD", "3")
|
||||||
|
.env("WAYSCRIBER_INTERNAL_PROCESS_BROKER_SHUTDOWN_FD", "4")
|
||||||
|
.env("WAYSCRIBER_INTERNAL_PROCESS_BROKER_TOKEN", "a".repeat(64)),
|
||||||
|
)
|
||||||
|
.failure()
|
||||||
|
.code(126);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_arguments_exit_two() {
|
fn invalid_arguments_exit_two() {
|
||||||
for (args, diagnostic) in [
|
for (args, diagnostic) in [
|
||||||
|
|||||||
@@ -334,11 +334,13 @@ internal static class ChecksCommand
|
|||||||
[RepositoryNames.Gtk4LayerShell] = [RepositoryNames.Gtk4LayerShell],
|
[RepositoryNames.Gtk4LayerShell] = [RepositoryNames.Gtk4LayerShell],
|
||||||
["input"] = ["libinput"],
|
["input"] = ["libinput"],
|
||||||
["ksni"] = [],
|
["ksni"] = [],
|
||||||
|
["lexopt"] = [],
|
||||||
["libc"] = [],
|
["libc"] = [],
|
||||||
["log"] = [],
|
["log"] = [],
|
||||||
["pango"] = ["pango"],
|
["pango"] = ["pango"],
|
||||||
["pangocairo"] = ["pango", "cairo"],
|
["pangocairo"] = ["pango", "cairo"],
|
||||||
["png"] = [],
|
["png"] = [],
|
||||||
|
["rustix"] = [],
|
||||||
["schemars"] = [],
|
["schemars"] = [],
|
||||||
["serde"] = [],
|
["serde"] = [],
|
||||||
["serde_ignored"] = [],
|
["serde_ignored"] = [],
|
||||||
|
|||||||
Reference in new issue
Block a user