# GitHub Issue #1496 Investigation Report

**Issue Title:** on freebsd 14 `service.service` loads `/etc/init.d/mysql-server` instead of `/usr/local/etc/rc.d/mysql-server`

**Reporter:** @albdv
**Status:** Open
**Date:** 2025-11-02

---

## Issue Summary

When using `server.service` to manage the `mysql-server` service on FreeBSD 14, pyinfra incorrectly detects the init system as sysvinit (Linux `/etc/init.d`) instead of BSD init (`/etc/rc.d` or `/usr/local/etc/rc.d`).

The user is running:
```python
server.service(
    name = f"Enable service mysql",
    service="mysql-server",
    running=True,
    enabled=True,
    _sudo=True,
)
```

The expected behavior is for pyinfra to use FreeBSD's native service management (`/usr/local/etc/rc.d/mysql-server`), but instead it attempts to run `/etc/init.d/mysql-server start`, which does not exist on FreeBSD.

---

## Root Cause Analysis

The root cause is in the `server.service()` operation at `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/operations/server.py` (lines 508-518).

The init system detection logic uses a series of `if/elif` statements to determine which service management system to use:

```python
elif (
    host.get_fact(Which, command="service")
    or host.get_fact(Link, path="/etc/init.d")
    or host.get_fact(Directory, path="/etc/init.d")
):
    service_operation = sysvinit.service

# NOTE: important that we are not Linux here because /etc/rc.d will exist but checking it's
# contents may trigger things (like a reboot: https://github.com/Fizzadar/pyinfra/issues/819)
elif host.get_fact(Os) != "Linux" and bool(host.get_fact(Directory, path="/etc/rc.d")):
    service_operation = bsdinit.service
```

**The Problem:**

1. FreeBSD has a `service` command (`/usr/sbin/service`) that is used to manage rc.d scripts.
2. The detection logic checks for `Which(command="service")` BEFORE checking for BSD-specific paths.
3. Since FreeBSD has the `service` command, the check `host.get_fact(Which, command="service")` returns `True`.
4. This causes the sysvinit handler to be selected, which looks for scripts in `/etc/init.d/` instead of BSD's `/etc/rc.d/` or `/usr/local/etc/rc.d/`.

**Why FreeBSD 14 is affected:**

FreeBSD has always had the `service` command, but the issue manifests because:
- The `service` command exists at `/usr/sbin/service`
- The detection logic prioritizes the presence of `service` command over OS-specific paths
- BSD init scripts are located in `/etc/rc.d/` (base system) and `/usr/local/etc/rc.d/` (ports/packages)

---

## Proposed Fix

### Option 1: Check OS before the `service` command check (Recommended)

Modify the detection logic to check for BSD systems BEFORE checking for the `service` command. The `service` command check should only apply to Linux sysvinit systems.

**File:** `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/operations/server.py`

**Change the service detection logic from:**

```python
elif (
    host.get_fact(Which, command="service")
    or host.get_fact(Link, path="/etc/init.d")
    or host.get_fact(Directory, path="/etc/init.d")
):
    service_operation = sysvinit.service

# NOTE: important that we are not Linux here because /etc/rc.d will exist but checking it's
# contents may trigger things (like a reboot: https://github.com/Fizzadar/pyinfra/issues/819)
elif host.get_fact(Os) != "Linux" and bool(host.get_fact(Directory, path="/etc/rc.d")):
    service_operation = bsdinit.service
```

**To:**

```python
# NOTE: Check for BSD init BEFORE sysvinit because FreeBSD also has a `service` command
# that would otherwise trigger the sysvinit detection. Important that we are not Linux
# here because /etc/rc.d will exist but checking its contents may trigger things
# (like a reboot: https://github.com/Fizzadar/pyinfra/issues/819)
elif host.get_fact(Os) != "Linux" and bool(host.get_fact(Directory, path="/etc/rc.d")):
    service_operation = bsdinit.service

elif (
    host.get_fact(Which, command="service")
    or host.get_fact(Link, path="/etc/init.d")
    or host.get_fact(Directory, path="/etc/init.d")
):
    service_operation = sysvinit.service
```

### Option 2: Exclude BSD from the `service` command check

Alternatively, modify the sysvinit detection to explicitly exclude BSD systems:

```python
elif (
    (host.get_fact(Which, command="service") and "BSD" not in host.get_fact(Os))
    or host.get_fact(Link, path="/etc/init.d")
    or host.get_fact(Directory, path="/etc/init.d")
):
    service_operation = sysvinit.service
```

### Option 3: Use the dedicated FreeBSD service module

Note that pyinfra already has a dedicated FreeBSD service module at `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/operations/freebsd/service.py` that uses the native `service` command with proper path detection via `service -l`.

The `server.service()` operation could be enhanced to detect FreeBSD specifically and delegate to this module:

```python
elif host.get_fact(Os) == "FreeBSD":
    from . import freebsd
    # Note: Would need to adapt the API since freebsd.service uses different parameters
    service_operation = freebsd.service.service
```

However, this would require API changes since the FreeBSD service operation uses different parameters (`srvstate` vs `running`/`restarted`/`reloaded`).

---

## Files That Need Modification

### Primary Change

1. **`/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/operations/server.py`** (lines 508-518)
   - Reorder the init system detection logic to check for BSD before checking for the `service` command

### Test Files to Update/Add

2. **`/Users/nick/Dev/Webroot/pyinfra/src/tests/operations/server.service/start_rcd.json`**
   - The current test already sets `"command=service": false`, which means FreeBSD without the service command
   - A new test should be added for FreeBSD WITH the service command present

3. **New test file:** `/Users/nick/Dev/Webroot/pyinfra/src/tests/operations/server.service/start_rcd_with_service_cmd.json`
   - Add a test case for FreeBSD that has the `service` command available to ensure BSD init is still selected

Example new test case:
```json
{
    "args": ["mysql-server"],
    "facts": {
        "server.Os": "FreeBSD",
        "bsdinit.RcdStatus": {
            "mysql-server": false
        },
        "server.Which": {
            "command=systemctl": false,
            "command=initctl": false,
            "command=rc-service": false,
            "command=sv": false,
            "command=service": "/usr/sbin/service"
        },
        "files.Directory": {
            "path=/etc/init.d": false,
            "path=/etc/rc.d": true
        },
        "files.Link": {
            "path=/etc/init.d": false
        }
    },
    "commands": [
        "test -e /etc/rc.d/mysql-server && /etc/rc.d/mysql-server start || /usr/local/etc/rc.d/mysql-server start"
    ]
}
```

---

## Additional Considerations

1. **OpenBSD and NetBSD:** The fix should also work for other BSD variants. OpenBSD also has a `service` command, and NetBSD may have similar characteristics.

2. **Backwards Compatibility:** The proposed fix (Option 1) should not break existing functionality for Linux systems since:
   - Linux systems will have `Os` = "Linux"
   - The BSD check explicitly excludes Linux (`host.get_fact(Os) != "Linux"`)
   - Linux sysvinit systems will still match the sysvinit detection logic

3. **The dedicated FreeBSD module:** Users who need more advanced FreeBSD service management (like jail support) should use the `pyinfra.operations.freebsd.service` module directly instead of the generic `server.service()`.

---

## Debug Output Analysis

From the issue reporter's debug output, we can see the detection sequence:

```
[pyinfra.api.facts] Getting fact: server.Which (command=service) (ensure_hosts: None)
[pyinfra.connectors.ssh] Command exit status: 0
[pyinfra.api.facts] [@vagrant/default] Loaded fact server.Which (command=service)
[pyinfra.api.facts] Getting fact: sysvinit.InitdStatus () (ensure_hosts: None)
```

The `server.Which (command=service)` returns successfully (exit code 0, meaning the `service` command exists), and then immediately the `sysvinit.InitdStatus` fact is loaded instead of `bsdinit.RcdStatus`, confirming that the sysvinit path was incorrectly taken.

---

## Recommendation

Implement **Option 1** (reorder the detection logic) as it is:
- The simplest change
- Maintains backwards compatibility
- Aligns with the existing comment about BSD systems
- Does not require API changes

The fix is straightforward and should resolve the issue for all BSD systems that have the `service` command.
