Skip to content
Merged
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0


## [Unreleased]
...
### Added
- `seabreeze_os_setup` preview the udev rules on linux before installing them

### Changed
- `seabreeze_os_setup` install the udev rules with mode `644` on linux

## [2.10.1] - 2025-01-29
### Fixed
Expand Down
6 changes: 4 additions & 2 deletions os_support/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ That depends on your operating system. Currently only Linux and Windows require
additional setup. OSX works ootb and is a no-op when running
`seabreeze_os_setup`

**On Linux** it downloads and copies the `10-oceanoptics.rules` file to
`/etc/udev/rules.d/` and runs `sudo udevadm control --reload-rules`.
**On Linux** it downloads the `10-oceanoptics.rules` file, prints its contents
for review, and, after you confirm, copies it to `/etc/udev/rules.d/` with mode
`644` and runs `sudo udevadm control --reload-rules`. If you decline, the rules
are not installed and the commands for installing them manually are printed.

**On Windows** it downloads and extracts the `windows-driver-files.zip` archive
and runs `pnputil -i -a *.inf` inside the extracted folder in an admin shell.
30 changes: 28 additions & 2 deletions src/seabreeze/os_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"https://raw.githubusercontent.com/ap--/python-seabreeze/master/os_support"
)
_UDEV_RULES_PATH = "/etc/udev/rules.d/10-oceanoptics.rules"
_UDEV_RULES_MODE = "644"
_DRIVERS_ZIP_FN = "windows-driver-files.zip"
_log = logging.getLogger(__name__)

Expand All @@ -35,6 +36,12 @@ def _diff_files(file1, file2):
return err.output.decode("utf8")


def _preview_file(filename):
"""return an indented preview of a file's contents"""
with open(filename, encoding="utf8") as f:
return indent(f.read().rstrip(), " ")


def _request_confirmation(question):
"""require user input to continue"""
while True:
Expand Down Expand Up @@ -100,12 +107,31 @@ def linux_install_udev_rules():
)
sys.exit(1)

# show the rules to the user before installing them
_log.info(f"The following udev rules will be installed as {_UDEV_RULES_PATH}:")
_log.info(_preview_file(udev_fn))

if not _request_confirmation("Install udev rules?"):
_log.info(
dedent(
f"""\
To install the rules manually, copy the rules shown above to
{_UDEV_RULES_PATH} and run:

sudo chmod {_UDEV_RULES_MODE} {_UDEV_RULES_PATH}
sudo udevadm control --reload-rules"""
)
)
sys.exit(0)

# cp rules and execute
# install rules and execute
_log.info(f"Copying udev rules to {_UDEV_RULES_PATH}")
subprocess.call(["sudo", "cp", udev_fn, _UDEV_RULES_PATH])
return_code = subprocess.call(
["sudo", "install", "-m", _UDEV_RULES_MODE, udev_fn, _UDEV_RULES_PATH]
)
if return_code != 0:
_log.error(f"Copying udev rules failed with return code {return_code}")
sys.exit(1)
_log.info("Calling udevadm control --reload-rules")
subprocess.call(["sudo", "udevadm", "control", "--reload-rules"])
_log.info("Success")
Expand Down
86 changes: 86 additions & 0 deletions tests/test_os_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""tests for the `seabreeze_os_setup` command line tool"""

import logging
import sys
from unittest import mock

import pytest

from seabreeze import os_setup


@pytest.fixture
def udev_rules_file(tmp_path):
"""a local rules file provided to the setup script"""
rules_file = tmp_path / "10-oceanoptics.rules"
rules_file.write_text("# oceanoptics test rules\n")
yield str(rules_file)


@pytest.fixture
def udev_rules_path(tmp_path, monkeypatch):
"""a not yet existing udev rules install location"""
rules_path = str(tmp_path / "rules.d" / "10-oceanoptics.rules")
monkeypatch.setattr(os_setup, "_UDEV_RULES_PATH", rules_path)
yield rules_path


@pytest.fixture
def subprocess_call():
with mock.patch.object(os_setup.subprocess, "call", return_value=0) as call:
yield call


def _run_linux_install_udev_rules(rules_file, monkeypatch, confirm):
monkeypatch.setattr(sys, "argv", ["seabreeze_os_setup", rules_file])
monkeypatch.setattr(os_setup, "_request_confirmation", lambda question: confirm)
with pytest.raises(SystemExit) as exc_info:
os_setup.linux_install_udev_rules()
return exc_info.value.code


def test_linux_install_udev_rules_previews_rules(
udev_rules_file, udev_rules_path, subprocess_call, monkeypatch, caplog
):
"""the rules are printed before they get installed"""
caplog.set_level(logging.INFO)
code = _run_linux_install_udev_rules(udev_rules_file, monkeypatch, confirm=True)

assert code == 0
assert "# oceanoptics test rules" in caplog.text
assert udev_rules_path in caplog.text


def test_linux_install_udev_rules_installs_with_644(
udev_rules_file, udev_rules_path, subprocess_call, monkeypatch
):
"""the rules are installed world readable"""
code = _run_linux_install_udev_rules(udev_rules_file, monkeypatch, confirm=True)

assert code == 0
assert (
mock.call(["sudo", "install", "-m", "644", udev_rules_file, udev_rules_path])
in subprocess_call.call_args_list
)


def test_linux_install_udev_rules_declined(
udev_rules_file, udev_rules_path, subprocess_call, monkeypatch, caplog
):
"""nothing is installed and manual instructions are shown"""
caplog.set_level(logging.INFO)
code = _run_linux_install_udev_rules(udev_rules_file, monkeypatch, confirm=False)

assert code == 0
assert subprocess_call.call_args_list == []
assert f"sudo chmod 644 {udev_rules_path}" in caplog.text


def test_linux_install_udev_rules_install_error(
udev_rules_file, udev_rules_path, subprocess_call, monkeypatch
):
"""a failing install command is reported as an error"""
subprocess_call.return_value = 1
code = _run_linux_install_udev_rules(udev_rules_file, monkeypatch, confirm=True)

assert code == 1
Loading