Hi I’m trying to build a python application and run it as a systemd service.
I have the following files:
- app
- controller.py
- monitor.py
- main.py
- default.nix
- pyproject.toml
- service.nix
Now to be honest I’m not that good with python or nix so I have no idea if it’s my nix code that is wrong or my python code.
I have used the following as reference to get me this far:
- Packaging an executable Python script nicely in Nix for NixOS
- GitHub - fzakaria/no-frills-nix-python-template: A simple Nix template for building and distributing Python applications
I can get my service to build with nixos-rebuild switch, but the problem is that main.py doesn’t seem to find the other files as I get the following error:
nov 18 21:20:27 brage systemd[1]: monitor-controllers.service: Failed with result 'exit-code'.
nov 18 21:20:27 brage systemd[1]: monitor-controllers.service: Main process exited, code=exited, status=1/FAILURE
nov 18 21:20:27 brage app[84062]: ModuleNotFoundError: No module named 'monitor'
nov 18 21:20:27 brage app[84062]: from monitor import Monitor
nov 18 21:20:27 brage app[84062]: File "/nix/store/f9jsrvkjrr1lj3ldv5vlhkjvk7kvjn4l-python3.12-monitor-controllers-0.0.1/lib/python3.12/site-packages/app/main.py", line 7, in <module>
nov 18 21:20:27 brage app[84062]: from app.main import start
nov 18 21:20:27 brage app[84062]: File "/nix/store/f9jsrvkjrr1lj3ldv5vlhkjvk7kvjn4l-python3.12-monitor-controllers-0.0.1/bin/.app-wrapped", line 6, in <module>
nov 18 21:20:27 brage app[84062]: Traceback (most recent call last):counter is at 8.
So I went into the /nix/store/f9jsrvkjrr1lj3ldv5vlhkjvk7kvjn4l-python3.12-monitor-controllers-0.0.1 folder and it has the following structure:
- bin
- app (bash script that starts .app-wrapped)
- .app-wrapped (python script that starts main.py I think)
- lib
- python3.12
- site-packages
- app
- controller.py
- monitor.py
- main.py
- app
- site-packages
- python3.12
pyproject.toml
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "monitor-controllers"
version = "0.0.1"
dependencies = [
"evdev"
]
[project.scripts]
app = "app.main:start"
[tool.setuptools]
packages = ["app"]
main.py
#!/usr/bin/env python3
import argparse
import asyncio
import logging
import signal
from monitor import Monitor
def setup_logging(level: str):
numeric = getattr(logging, level.upper(), None)
if not isinstance(numeric, int):
numeric = logging.INFO
logging.basicConfig(level=numeric, format="%(asctime)s %(levelname)s: %(message)s")
async def main():
parser = argparse.ArgumentParser(description="Disconnect idle PS3/PS4 Bluetooth controllers")
parser.add_argument("--timeout-minutes", type=int, default=10, help="Idle timeout in minutes")
parser.add_argument("--log-level", default="INFO", help="Logging level (DEBUG, INFO, WARNING, ERROR)")
args = parser.parse_args()
setup_logging(args.log_level)
monitor = Monitor(args.timeout_minutes)
loop = asyncio.get_event_loop()
def stop(signum, frame):
logging.info("Received signal %s — shutting down", signum)
loop.call_soon_threadsafe(monitor.stop)
signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
logging.info("Starting monitor (timeout=%d minutes)", args.timeout_minutes)
try:
await monitor.start()
finally:
monitor.stop()
def start():
asyncio.run(main())
monitor.py
from controller import Controller
class Monitor:
def __init__(self, timeout_minutes):
.....
default.nix
{ nixpkgs ? import <nixpkgs> {}, pythonPkgs ? nixpkgs.pkgs.python3Packages }:
let
# This takes all Nix packages into this scope
inherit (nixpkgs) pkgs;
# This takes all Python packages from the selected version into this scope.
inherit pythonPkgs;
# Inject dependencies into the build function
f = { buildPythonPackage, evdev, buildtools }:
buildPythonPackage rec {
pname = "monitor-controllers";
version = "0.0.1";
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
format = "pyproject";
propagatedBuildInputs = [ evdev setuptools ];
meta = {
description = ''
Disconnect idle PS3/PS4 Bluetooth controllers
'';
};
};
drv = pythonPkgs.callPackage f {};
in
if pkgs.lib.inNixShell then drv.env else drv
service.nix
{ config, lib, pkgs, ... }:
let
monitor-controllers = pkgs.callPackage ./default.nix {};
cfg = config.services.monitor-controllers;
in {
options.services.monitor-controllers.log-level = lib.mkOption {
type = lib.types.str;
default = "warning";
example = "debug";
};
options.services.monitor-controllers.timeout-minutes = lib.mkOption {
type = lib.types.int;
default = 5;
};
# Everything that should be done when/if the service is enabled
config = lib.mkIf cfg.enable {
systemd.services.monitor-controllers = {
description = "Disconnect idle PS3/PS4 Bluetooth controllers";
environment = {
PYTHONUNBUFFERED = "1";
};
after = [ "multi-user.target.target" ];
wantedBy = [ "multi-user.target.target" ];
serviceConfig = {
ExecStart = "${monitor-controllers}/bin/app --timeout-minutes ${toString cfg.log-level} --log_level ${toString cfg.timeout-minutes}";
Restart = "on-failure";
RestartSec = "5";
User = "root";
};
};
};
}
app (Generated when i build?)
#! /nix/store/cfqbabpc7xwg8akbcchqbq3cai6qq2vs-bash-5.2p37/bin/bash -e
PATH=${PATH:+':'$PATH':'}
PATH=${PATH/':''/nix/store/f9jsrvkjrr1lj3ldv5vlhkjvk7kvjn4l-python3.12-monitor-controllers-0.0.1/bin'':'/':'}
PATH='/nix/store/f9jsrvkjrr1lj3ldv5vlhkjvk7kvjn4l-python3.12-monitor-controllers-0.0.1/bin'$PATH
PATH=${PATH#':'}
PATH=${PATH%':'}
export PATH
PATH=${PATH:+':'$PATH':'}
PATH=${PATH/':''/nix/store/vxl8pzgkkw8vdb4agzwm58imrfclmfrx-python3-3.12.11/bin'':'/':'}
PATH='/nix/store/vxl8pzgkkw8vdb4agzwm58imrfclmfrx-python3-3.12.11/bin'$PATH
PATH=${PATH#':'}
PATH=${PATH%':'}
export PATH
export PYTHONNOUSERSITE='true'
exec -a "$0" "/nix/store/f9jsrvkjrr1lj3ldv5vlhkjvk7kvjn4l-python3.12-monitor-controllers-0.0.1/bin/.app-wrapped" "$@"
.app-wrapped (Generated when i build?)
#!/nix/store/vxl8pzgkkw8vdb4agzwm58imrfclmfrx-python3-3.12.11/bin/python3.12
# -*- coding: utf-8 -*-
import sys;import site;import functools;sys.argv[0] = '/nix/store/f9jsrvkjrr1lj3ldv5vlhkjvk7kvjn4l-python3.12-monitor-controllers-0.0.1/bin/app';functools.reduce(lambda k, p: site.addsitedir(p, k), ['/nix/store/f9jsrvkjrr1lj3ldv5vlhkjvk7kvjn4l-python3.12-monitor-controllers-0.0.1/lib/python3.12/site-packages','/nix/store/wrasigx2g79vfzsy2ygkcdn453lh3ccz-python3.12-evdev-1.9.2/lib/python3.12/site-packages','/nix/store/dklqz633x8big8lgqifpj97f8hd7v6b8-python3.12-setuptools-78.1.1/lib/python3.12/site-packages'], site._init_pathinfo());
import re
import sys
from app.main import start
if __name__ == "__main__":
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
sys.exit(start())
after importing service.nix I can add the following to my config to enable the service
services.monitor-controllers = {
enable = true;
log-level = "debug";
timeout-minutes = 1;
};
Sorry for the wall of text, the idea was to upload the code to a git repo but github seems to have some issues currently.