Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
# Chrome is unused; its third-party index must not block Ubuntu dependencies.
sudo rm -fv /etc/apt/sources.list.d/google-chrome.list /etc/apt/sources.list.d/google-chrome.sources
sudo apt-get update
sudo apt-get install -y at-spi2-core bubblewrap dbus-x11 gjs libadwaita-1-dev libglib2.0-bin libgtk-4-dev pkexec python3-dbus python3-pyatspi xdotool xvfb
sudo apt-get install -y at-spi2-core bubblewrap dbus-x11 gjs libadwaita-1-dev libglib2.0-bin libgtk-4-dev pkexec python3-dbus python3-pyatspi util-linux xdotool xvfb

- name: Enable setup CLI test namespaces
# Match the disposable-runner preparation in the bundle smoke job.
Expand Down Expand Up @@ -248,7 +248,7 @@ jobs:
# Chrome is unused; its third-party index must not block Ubuntu dependencies.
sudo rm -fv /etc/apt/sources.list.d/google-chrome.list /etc/apt/sources.list.d/google-chrome.sources
sudo apt-get update
sudo apt-get install -y at-spi2-core binutils dbus-x11 libadwaita-1-dev libgtk-4-dev pkexec python3-dbus python3-pyatspi strace xdotool xvfb
sudo apt-get install -y at-spi2-core binutils dbus-x11 libadwaita-1-dev libgtk-4-dev pkexec python3-dbus python3-pyatspi strace util-linux xdotool xvfb
# Prove current installation and GUI behavior without the retired dialog tool or external idle command.
sudo apt-get purge -y zenity swayidle

Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ are strongly recommended so the saved address stays valid and the TV remains
ready to respond.

Official bundles include the GTK desktop app and CLI together. The installer
checks for GTK 4.14 and libadwaita 1.5 or newer, offers to install missing
packages on Debian/Ubuntu, Fedora, and Arch with your confirmation, and verifies
the requirements before proceeding. The prebuilt binaries require glibc 2.39
or newer—the Ubuntu 24.04 runtime baseline.
installs `util-linux` if `flock` is missing, and checks for GTK 4.14 and
libadwaita 1.5 or newer. It offers to install missing GTK packages on
Debian/Ubuntu, Fedora, and Arch with your confirmation, and verifies the
requirements before proceeding. The prebuilt binaries require glibc 2.39 or
newer—the Ubuntu 24.04 runtime baseline.
The installed runtime does not require Python, pip, or bscpylgtv.
TV Sleep & Wake activation requires `pkexec` and
a desktop authorization agent. You can also install the prerequisites manually:
Expand All @@ -77,19 +78,19 @@ a desktop authorization agent. You can also install the prerequisites manually:
### Debian, Ubuntu, and Pop!_OS

```bash
sudo apt install libgtk-4-1 libadwaita-1-0 pkexec
sudo apt install libgtk-4-1 libadwaita-1-0 pkexec util-linux
```

### Fedora

```bash
sudo dnf install gtk4 libadwaita polkit
sudo dnf install gtk4 libadwaita polkit util-linux
```

### Arch Linux

```bash
sudo pacman -S gtk4 libadwaita polkit
sudo pacman -S gtk4 libadwaita polkit util-linux
```

</details>
Expand Down
30 changes: 23 additions & 7 deletions crates/lg-buddy/src/setup/authorization/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ printf 'helper diagnostic' >&2
[ "${2:-}" != fail ] || exit 1
"#,
);
// Lock Bash's fd 9 through the test binary; the shell retains the
// locked open-file description after the child exits.
fs::write(
fixture.0.join("plasma.sh"),
format!(
Expand All @@ -113,7 +115,7 @@ foreground=0
allow_dependencies=0
main() {{
exec 9>'{root}/plasma.lock'
flock -n 9 || return 1
LG_BUDDY_PLASMA_LOCK_CHILD=1 '{test_exe}' --exact setup::authorization::tests::plasma_lock_child --quiet >/dev/null || return 1
[ "$1" = --foreground ] || return 1
printf '%s\n' "$*" >> '{root}/plasma-options'
for option in "${{@:2}}"; do
Expand All @@ -131,7 +133,8 @@ main() {{
privileged --system-install 1000 root id source
}}
"#,
root = fixture.0.display()
root = fixture.0.display(),
test_exe = std::env::current_exe().unwrap().display()
),
)
.unwrap();
Expand Down Expand Up @@ -435,12 +438,26 @@ fn terminal_services_and_plasma_share_parent_scoped_sudo_permission() {
assert!(fixture.0.join("sudo-grant").exists());
}

#[test]
fn plasma_lock_child() {
if std::env::var_os("LG_BUDDY_PLASMA_LOCK_CHILD").is_none() {
return;
}
assert_eq!(
unsafe { libc::flock(9, libc::LOCK_EX | libc::LOCK_NB) },
0,
"{}",
std::io::Error::last_os_error()
);
}

#[test]
fn terminal_prompt_child() {
let Some(root) = std::env::var_os("LG_BUDDY_TERMINAL_AUTHORIZATION_ROOT") else {
return;
};
let fixture = Fixture(root.into());
// The parent owns this directory, including when an assertion fails here.
let fixture = std::mem::ManuallyDrop::new(Fixture(root.into()));
let session = fixture.session_for_mode(AuthorizationMode::Terminal, None);
assert!(session
.services(Path::new("config"), None)
Expand All @@ -453,7 +470,6 @@ fn terminal_prompt_child() {
.status
.success());
session.close();
std::mem::forget(fixture);
}

#[test]
Expand Down Expand Up @@ -801,12 +817,12 @@ fn authorization_owner_child() {
let Some(root) = std::env::var_os("LG_BUDDY_AUTHORIZATION_TEST_ROOT") else {
return;
};
let fixture = Fixture(root.into());
// The parent owns this directory, including if this child exits early.
let fixture = std::mem::ManuallyDrop::new(Fixture(root.into()));
let lease = super::super::lock::FlowLock::try_acquire(&fixture.0.join("flow.lock")).unwrap();
let session = fixture.session(Some(&lease.file()));
let _ = session.plasma(&fixture.0.join("plasma.sh"), false, None);
// The parent kills this owner during the call; only it owns fixture cleanup.
std::mem::forget(fixture);
// The parent kills this owner during the call and cleans up the fixture.
}

#[test]
Expand Down
16 changes: 16 additions & 0 deletions crates/lg-buddy/src/setup/flow/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ fn modal_recheck_cannot_retry_a_still_active_helper() {
gui::{OnboardingApplication, OnboardingBackend, OnboardingIntent},
published::{SetupSnapshot, SnapshotBackend},
};
use std::time::{Duration, Instant};
struct Backend {
state: Arc<Mutex<State>>,
path: PathBuf,
Expand Down Expand Up @@ -359,6 +360,21 @@ fn modal_recheck_cannot_retry_a_still_active_helper() {
assert_eq!(fixture.state.lock().unwrap().calls, [SetupStep::Services]);
}
backend.helper_lease.lock().unwrap().take();
// A parallel subprocess may briefly inherit the old descriptor before exec.
let deadline = Instant::now() + Duration::from_secs(2);
loop {
match FlowLock::try_acquire(&backend.path) {
Ok(lease) => {
drop(lease);
break;
}
Err(error) => {
assert_eq!(error.kind(), std::io::ErrorKind::WouldBlock);
assert!(Instant::now() < deadline, "helper retained the setup lock");
std::thread::sleep(Duration::from_millis(5));
}
}
}
let operation = app
.handle(OnboardingIntent::Submit)
.unwrap()
Expand Down
14 changes: 12 additions & 2 deletions data/kwin/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,16 @@ load_installed() {
return 0
}

lock_setup() {
local status=0
flock -n 9 || status=$?
if [ "$status" -eq 127 ]; then
echo "LG Buddy KWin setup requires flock (provided by util-linux)." >&2
return 1
fi
return "$status"
}

main() {
# Once authorized, operation failures must not resemble pkexec's 126/127.
case "${1:-}" in --system-*) system_action "$@" || return 1; return 0 ;; esac
Expand All @@ -326,7 +336,7 @@ main() {
if [ -d "$state_dir" ]; then
log_file="$state_dir/setup.log"
exec 9>"$state_dir/setup.lock"
flock -n 9 || return 1
lock_setup || return 1
remove_previous || return $?
fi
rm -rf -- "$cache_dir"
Expand Down Expand Up @@ -354,7 +364,7 @@ main() {
mkdir -p -- "$state_dir/plugins" "$cache_dir" || return 1
chmod 700 "$state_dir" "$cache_dir" || return 1
exec 9>"$state_dir/setup.lock"
flock -n 9 || return 1
lock_setup || return 1
# Recheck after taking ownership; another setup may just have completed.
if inspect_session; then return 0; else status=$?; fi
[ "$status" -eq 3 ] || return "$status"
Expand Down
2 changes: 2 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Compiling the Rust runtime requires:
- a Rust toolchain with `cargo`
- a working C toolchain

KWin setup also requires `flock` from `util-linux`.

Compiling and testing the GTK frontend additionally requires:

- GTK 4.10 or newer development files
Expand Down
1 change: 1 addition & 0 deletions docs/kwin-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ supported reduced coverage; an applicable unmet setup requirement remains pendin
The native installer installs an optional `LG_Buddy_kwin.service` user unit and
the bridge payload under `/usr/lib/lg-buddy/kwin`. Session activation can load an
already installed plugin; it never provisions or requests authorization.
The helper uses `flock` from `util-linux` to serialize setup and removal.
Explicit foreground setup tries:

1. A compatible bundled prebuilt.
Expand Down
24 changes: 24 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,29 @@ check_dep() {
fi
}

require_flock() {
local flock_path="${LG_BUDDY_TEST_FLOCK_PATH:-/usr/bin/flock}"
if [ -x "$flock_path" ]; then
echo " [OK] flock (KWin setup)"
return 0
fi

echo "Installing required util-linux (provides flock)..."
if [ -z "$PM" ] || ! run_privileged "${INSTALL_CMD[@]}" util-linux; then
echo "Error: could not install required util-linux." >&2
MISSING_PKGS=(util-linux)
print_manual_install_command
exit 1
fi
if [ ! -x "$flock_path" ]; then
echo "Error: util-linux was installed but flock is still unavailable." >&2
MISSING_PKGS=(util-linux)
print_manual_install_command
exit 1
fi
echo " [OK] flock (KWin setup)"
}

detect_package_manager() {
if command -v apt &>/dev/null; then
PM="apt"
Expand Down Expand Up @@ -777,6 +800,7 @@ check_install_prerequisites() {
echo "Checking prerequisites..."
MISSING_PKGS=()
detect_package_manager
require_flock
check_gui_runtime_prerequisites
if [ "$FRESH_SETUP_MODE" -eq 1 ] && [ "$HEADLESS" -eq 0 ]; then
check_dep "pkexec (required for TV Sleep & Wake)" "$(pkexec_package)" "pkexec_available"
Expand Down
79 changes: 78 additions & 1 deletion scripts/test-installer-gui-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,20 @@ case "$PACKAGE_MANAGER" in
apt)
EXPECTED_ARGS="install -y libgtk-4-1 libadwaita-1-0"
EXPECTED_MANUAL="sudo apt install libgtk-4-1 libadwaita-1-0"
EXPECTED_FLOCK_ARGS="install -y util-linux"
EXPECTED_FLOCK_MANUAL="sudo apt install util-linux"
;;
dnf)
EXPECTED_ARGS="install -y gtk4 libadwaita"
EXPECTED_MANUAL="sudo dnf install gtk4 libadwaita"
EXPECTED_FLOCK_ARGS="install -y util-linux"
EXPECTED_FLOCK_MANUAL="sudo dnf install util-linux"
;;
pacman)
EXPECTED_ARGS="-S --noconfirm gtk4 libadwaita"
EXPECTED_MANUAL="sudo pacman -S gtk4 libadwaita"
EXPECTED_FLOCK_ARGS="-S --noconfirm util-linux"
EXPECTED_FLOCK_MANUAL="sudo pacman -S util-linux"
;;
*) usage ;;
esac
Expand All @@ -42,6 +48,8 @@ PACKAGE_LOG="$WORK_DIR/package-manager.log"
SEQUENCE_LOG="$WORK_DIR/sequence.log"
GUI_WRAPPER="$WORK_DIR/lg-buddy-gui"
PROBE="$WORK_DIR/gui-runtime-probe"
REAL_FLOCK="$(command -v flock)"
TEST_FLOCK="$STUB_DIR/flock"

cleanup() {
rm -rf "$WORK_DIR"
Expand All @@ -66,6 +74,9 @@ fi
if [ "${LG_BUDDY_FAKE_INSTALL_SATISFIES:-1}" = "1" ]; then
: >"${LG_BUDDY_DEPENDENCIES_INSTALLED:?}"
fi
if [ "${LG_BUDDY_FAKE_INSTALL_FLOCK:-0}" = "1" ]; then
ln -s "${LG_BUDDY_REAL_FLOCK:?}" "${LG_BUDDY_STUB_DIR:?}/flock"
fi
EOF

cat >"$STUB_DIR/systemctl" <<'EOF'
Expand Down Expand Up @@ -97,6 +108,9 @@ run_fresh_install() {
local auto_install="$3"
local install_satisfies="$4"
local package_manager_fails="${5:-0}"
local command_path="${6:-$PATH}"
local install_flock="${7:-0}"
local flock_path="${8:-$REAL_FLOCK}"
local root="$WORK_DIR/$scenario/root"
local home="$WORK_DIR/$scenario/home"

Expand All @@ -108,7 +122,7 @@ tvs_primary_input=HDMI_1
tvs_primary_platform=lg_webos
EOF
(
export PATH="$STUB_DIR:$PATH"
export PATH="$STUB_DIR:$command_path"
export HOME="$home"
export XDG_CONFIG_HOME="$home/.config"
unset LG_BUDDY_CONFIG
Expand All @@ -129,6 +143,10 @@ EOF
export LG_BUDDY_REAL_GUI="$GUI_BINARY"
export LG_BUDDY_FAKE_INSTALL_SATISFIES="$install_satisfies"
export LG_BUDDY_FAKE_PACKAGE_MANAGER_FAIL="$package_manager_fails"
export LG_BUDDY_FAKE_INSTALL_FLOCK="$install_flock"
export LG_BUDDY_REAL_FLOCK="$REAL_FLOCK"
export LG_BUDDY_STUB_DIR="$STUB_DIR"
export LG_BUDDY_TEST_FLOCK_PATH="$flock_path"
if [ "$auto_install" = "1" ]; then
export LG_BUDDY_AUTO_INSTALL_DEPS="yes"
else
Expand Down Expand Up @@ -205,6 +223,65 @@ fi
[ -x "$WORK_DIR/success/root/usr/bin/lg-buddy" ]
[ -x "$WORK_DIR/success/root/usr/bin/lg-buddy-gui" ]

# Reproduce an installation environment with no flock, while keeping the
# package manager and other prerequisites available for the installer.
FILTERED_PATH=""
path_index=0
while IFS= read -r path_entry; do
[ -n "$path_entry" ] || continue
mirror_dir="$WORK_DIR/without-flock/path-$path_index"
mkdir -p "$mirror_dir"
for command_path_entry in "$path_entry"/*; do
[ -e "$command_path_entry" ] || continue
[ "$(basename "$command_path_entry")" = flock ] && continue
ln -s "$command_path_entry" "$mirror_dir/$(basename "$command_path_entry")"
done
FILTERED_PATH="${FILTERED_PATH:+$FILTERED_PATH:}$mirror_dir"
path_index=$((path_index + 1))
done < <(printf '%s\n' "$PATH" | tr ':' '\n')

touch "$DEPENDENCIES_INSTALLED"
rm -f "$PACKAGE_LOG" "$SEQUENCE_LOG"
ISOLATED_PATH_OUTPUT="$WORK_DIR/isolated-path.output"
if ! run_fresh_install isolated-path "$ISOLATED_PATH_OUTPUT" 0 1 0 "$FILTERED_PATH" 0 "$REAL_FLOCK"; then
cat "$ISOLATED_PATH_OUTPUT"
echo "Install failed when flock existed outside the isolated PATH." >&2
exit 1
fi
[ ! -e "$PACKAGE_LOG" ]
[ -x "$WORK_DIR/isolated-path/root/usr/bin/lg-buddy" ]

FLOCK_UNAVAILABLE_OUTPUT="$WORK_DIR/flock-unavailable.output"
if run_fresh_install flock-unavailable "$FLOCK_UNAVAILABLE_OUTPUT" 0 1 1 "$FILTERED_PATH" 0 "$TEST_FLOCK"; then
echo "Install unexpectedly continued after util-linux installation failed." >&2
exit 1
fi
grep -Fq 'Installing required util-linux (provides flock)' "$FLOCK_UNAVAILABLE_OUTPUT"
grep -Fq 'could not install required util-linux' "$FLOCK_UNAVAILABLE_OUTPUT"
grep -Fq "$EXPECTED_FLOCK_MANUAL" "$FLOCK_UNAVAILABLE_OUTPUT"
[ "$(cat "$PACKAGE_LOG")" = "$EXPECTED_FLOCK_ARGS" ]
[ -z "$(find "$WORK_DIR/flock-unavailable/root" -mindepth 1 -print -quit)" ]

FLOCK_UNSATISFIED_OUTPUT="$WORK_DIR/flock-unsatisfied.output"
rm -f "$PACKAGE_LOG" "$SEQUENCE_LOG"
if run_fresh_install flock-unsatisfied "$FLOCK_UNSATISFIED_OUTPUT" 1 1 0 "$FILTERED_PATH" 0 "$TEST_FLOCK"; then
echo "Install unexpectedly accepted util-linux without flock." >&2
exit 1
fi
grep -Fq 'util-linux was installed but flock is still unavailable' "$FLOCK_UNSATISFIED_OUTPUT"
[ "$(cat "$PACKAGE_LOG")" = "$EXPECTED_FLOCK_ARGS" ]
[ -z "$(find "$WORK_DIR/flock-unsatisfied/root" -mindepth 1 -print -quit)" ]

FLOCK_SUCCESS_OUTPUT="$WORK_DIR/flock-success.output"
rm -f "$PACKAGE_LOG" "$SEQUENCE_LOG"
if ! run_fresh_install flock-success "$FLOCK_SUCCESS_OUTPUT" 0 1 0 "$FILTERED_PATH" 1 "$TEST_FLOCK"; then
cat "$FLOCK_SUCCESS_OUTPUT"
echo "Install failed after installing required util-linux." >&2
exit 1
fi
[ "$(cat "$PACKAGE_LOG")" = "$EXPECTED_FLOCK_ARGS" ]
[ -x "$WORK_DIR/flock-success/root/usr/bin/lg-buddy" ]

# The real candidate checks its loaded libraries without a graphical session.
env -u DISPLAY -u WAYLAND_DISPLAY "$GUI_BINARY" --check-runtime
[ ! -e "$WORK_DIR/success/root/usr/bin/LG_Buddy_PIP" ]
Expand Down
Loading
Loading