Metadata-Version: 2.4
Name: etiket_service_manager
Version: 0.3.0b1
Summary: Cross-platform service management library for Linux, macOS, and Windows
Author: QHarbor team
License-Expression: LicenseRef-Proprietary
Project-URL: Homepage, https://qharbor.nl
Project-URL: Documentation, https://docs.qharbor.nl
Keywords: service-management,systemd,launchd,scheduled-tasks,daemon,background-process
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: packaging>=21.0
Requires-Dist: psutil>=5.9.0
Requires-Dist: pywin32>=306; sys_platform == "win32"
Provides-Extra: test
Requires-Dist: pytest>=7.0.0; extra == "test"
Requires-Dist: httpx>=0.24.0; extra == "test"
Provides-Extra: dev
Requires-Dist: pyinstaller>=6.0.0; extra == "dev"
Requires-Dist: fastapi>=0.100.0; extra == "dev"
Requires-Dist: uvicorn[standard]>=0.23.0; extra == "dev"
Dynamic: license-file

# Service Manager

A cross-platform library for installing and managing services on Windows, macOS, and Linux.

## Overview

The Service Manager library provides a unified interface for managing system services across different operating systems. It follows standard service management conventions, allowing you to:

* Install/uninstall services
* Enable/disable services at boot time
* Start/stop running services
* Query service status

It creates user-level services (not system-wide root services), which means:
* **Linux**: User systemd services (`systemctl --user`)
* **macOS**: LaunchAgents (`~/Library/LaunchAgents`)
* **Windows**: Scheduled Tasks (running on user login)

## Installation

```bash
pip install etiket-service-manager
```

## Pre-configured Services

This library includes pre-configured service definitions for the QHarbor application suite. These helper functions automatically set up the correct paths, executable locations, and naming conventions for QHarbor's sync components.

```python
from etiket_service_manager.services import sync_agent, sync_api
from packaging.version import Version

# Get the manager for the Sync Agent
agent_manager = sync_agent.get_service()
agent_manager.status  # Check status

# Install — no need to pass program_arguments, the config carries them
agent_manager.install(version=Version("1.0.0"))

# Get the manager for the Sync API
api_manager = sync_api.get_service()
api_manager.install(version=Version("1.0.0"))
```

The pre-configured services resolve their executable via `default_runner_install_location()`, which knows the canonical location of the runner on each OS:

| Platform | Data directory (`app_dir`) | Runner executable |
|----------|----------------------------|-------------------|
| macOS    | `~/Library/Application Support/qharbor` | `~/Applications/qharbor/eTiKeT Service.app/Contents/MacOS/etiket_service_runner` |
| Linux    | `~/.local/share/qharbor` (or `$XDG_DATA_HOME/qharbor`) | `<app_dir>/etiket_service_runner` |
| Windows  | `%LOCALAPPDATA%\qharbor` | `<app_dir>\etiket_service_runner.exe` |

On macOS the runner ships as a code-signed `.app` bundle so it's identifiable to Gatekeeper, TCC, and System Settings (Privacy & Security → Full Disk Access). On Linux and Windows the bare binary lives in the per-user data directory.

## Actions and Behaviors

The following table describes each action, its expected behavior, possible errors, and resulting service states:

| Action | Description | Possible Errors | Expected Status After Action |
|--------|-------------|----------------|---------------------------|
| **install** | Installs, enables, and starts the service | `ServiceAlreadyInstalled`, `ServiceOperationError` | installed=True, enabled=True, running=True |
| **enable** | Enables the service to start automatically at boot | `ServiceAlreadyEnabled`, `ServiceOperationError` | installed=True, enabled=True, running=unchanged |
| **start** | Starts the service | `ServiceAlreadyStarted`, `ServiceOperationError` | installed=True, enabled=unchanged, running=True |
| **stop** | Stops the service | `ServiceAlreadyStopped`, `ServiceOperationError` | installed=True, enabled=unchanged, running=False |
| **disable** | Disables the service (prevents starting at boot) | `ServiceAlreadyDisabled`, `ServiceOperationError` | installed=True, enabled=False, running=False |
| **uninstall** | Uninstalls the service | `ServiceAlreadyUnInstalled`, `ServiceOperationError` | installed=False, enabled=False, running=False |

## Configuring the Service Manager

To use the Service Manager for your own applications, you need to create a `ServiceConfig` object. `exe_install_location` is required and uses a `PlatformPath` so the same config works across OSes — the entry matching the running OS is selected at install time.

```python
from pathlib import Path
from etiket_service_manager import PlatformPath, ServiceConfig, ServiceManager

config = ServiceConfig(
    service_name="my_service",
    app_dir=Path.home() / ".my_service",        # Runtime data: logs, state
    exe_install_location=PlatformPath(
        darwin=Path("/Applications/MyService.app/Contents/MacOS/my_service"),
        linux=Path.home() / ".local/bin/my_service",
        windows=Path.home() / "AppData/Local/MyService/my_service.exe",
    ),
    default_args=["--serve"],                   # Optional: appended to the executable
)

manager = ServiceManager(config)
```

### `ServiceConfig` fields

| Field | Type | Required | Purpose |
|-------|------|----------|---------|
| `service_name` | `str` | yes | Service identifier (used in plist label, systemd unit name, scheduled task name). |
| `app_dir` | `Path` | yes | Per-service runtime directory for logs and state. Created if missing. |
| `exe_install_location` | `PlatformPath` | yes | Per-OS absolute path to the installed executable. Resolved to a single path at install time. |
| `default_args` | `list[str] \| None` | no | Arguments appended after the resolved executable when `manager.install()` is called without `program_arguments`. |

### `PlatformPath`

A dataclass that holds one path per OS and resolves to the correct one based on `platform.system()`:

```python
from etiket_service_manager import PlatformPath

p = PlatformPath(
    darwin=Path("/usr/local/bin/foo"),
    linux=Path("/usr/bin/foo"),
    windows=Path(r"C:\Program Files\foo\foo.exe"),
)
p.resolve()  # → the path matching the running OS
```

## Example Usage

```python
from pathlib import Path

from packaging.version import Version

from etiket_service_manager import PlatformPath, ServiceConfig, ServiceManager


def main():
    # 1. Configure: declare where the executable lives on each OS and the
    #    default invocation. The same config works across all three OSes.
    config = ServiceConfig(
        service_name="my_service",
        app_dir=Path.home() / ".my_service",
        exe_install_location=PlatformPath(
            darwin=Path("/Applications/MyService.app/Contents/MacOS/my_service"),
            linux=Path.home() / ".local/bin/my_service",
            windows=Path.home() / "AppData/Local/MyService/my_service.exe",
        ),
        default_args=["--serve"],
    )
    manager = ServiceManager(config)

    # 2. Install. With exe_install_location + default_args set on the config,
    #    program_arguments is inferred — no need to pass it.
    manager.install(
        version=Version("1.0.0"),
        raise_if_already_installed=True,
    )

    # If you need a one-off override, pass program_arguments explicitly:
    # manager.install(version=Version("1.0.0"),
    #                 program_arguments=["/custom/path", "--debug"])

    # 3. Check status
    print(f"Service status: {manager.status}")

    # 4. Stop / disable / uninstall as needed
    manager.stop()
    manager.disable()
    manager.uninstall()


if __name__ == "__main__":
    main()
```

---

# Platform Specific Details

## Windows Services

This application is implemented as a **Scheduled Task** rather than a Windows service. A scheduled task can run without administrator permissions, unlike Windows services.

The scheduled task:
- Runs as a background process with `wscript` (hidden window).
- Starts automatically when the user logs in.
- Provides support for process recovery when it terminates unexpectedly.

### Requirements
- The service name is used as the Task Name.
- Tasks are created with "LeastPrivilege" run level, so no UAC prompt is needed.

### Logging
- **Behavior**: Unlike Linux/macOS, Windows Task Scheduler does **not** capture or redirect Standard Output (`stdout`) or Error (`stderr`). Any output printed to the console is lost.
- **Requirement**: Your application **must** internally handle logging to files.
  - Do not rely on `print()`.
  - Use a logging library (e.g., Python's `logging` module) to write directly to a file.
- **Suggested Location**: `{app_dir}/logs/` to match other platforms.

## macOS Services

This document outlines the process for creating and managing macOS services using `launchctl`. These services run in the GUI session of the logged-in user (LaunchAgents).

### Installation Locations
- **Service configuration file**: `~/Library/LaunchAgents/com.quantum-machines.{service_name}.plist`
- **Executable**: ship as a code-signed `.app` bundle in `~/Applications/{company}/{app_name}.app` (per-user, no admin required, Finder-discoverable for Privacy & Security grants) or `/Applications/{app_name}.app` (system-wide). For QHarbor's `etiket_service_runner` the canonical path is `~/Applications/qharbor/eTiKeT Service.app/Contents/MacOS/etiket_service_runner`.
- **Runtime data (`app_dir`)**: kept separate from the executable, in `~/Library/Application Support/{company}/`.
- **Logs**: `{app_dir}/{service_name}_logs/`

> [!IMPORTANT]
> Ship the executable as a `.app` bundle (with `CFBundleIdentifier`, `CFBundleExecutable`, `LSUIElement=true` for background services, and any `NSXxxUsageDescription` keys the agent needs). Bare Mach-O binaries cannot be cleanly granted permissions in System Settings → Privacy & Security and don't trigger TCC prompts with usable descriptions. The bundle ID + signing identity are what TCC keys grants on, so keep them stable across releases.

> [!IMPORTANT]
> The service is configured with `WorkingDirectory` set to the `app_dir`. If your application uses relative paths for resources or imports, the executable still belongs in the `.app` bundle — `app_dir` is for runtime data only.

The service definition (plist) includes:
- `KeepAlive`: True (system attempts to restart it if it crashes)
- `RunAtLoad`: True (starts immediately upon load/login)
- `ThrottleInterval`: 60 seconds


## Linux Services

Service in Linux are implemented using **systemd** user services. This is a robust system that works very well for user-level background processes.

### Locations
- **Configuration**: The systemd user unit file (`{service_name}.service`) is created in `~/.config/systemd/user/`.
- **Logs**: Managed by systemd. View logs using:
  ```bash
  journalctl --user -u {service_name}
  ```
  - Use `-f` to follow logs in real-time.
  - Use `-e` to jump to the end.
- **Application Binary**: The application executable should be in a stable, user-accessible location.
  - Recommended: `~/.local/bin/` for standalone binaries.
  - Note: Self-contained application directories often reside in `~/.local/share/`.
  - **Important**: Always provide the **absolute path** to the executable when installing the service.

### Behavior
- Uses `systemctl --user` commands to manage the service.
- defined as `Type=simple`.
- Configured with `Restart=always` and a 5-second delay to ensure reliability.

