diff --git a/.github/workflows/debian-compatibility.yml b/.github/workflows/debian-compatibility.yml new file mode 100644 index 0000000..fed024a --- /dev/null +++ b/.github/workflows/debian-compatibility.yml @@ -0,0 +1,27 @@ +name: Debian Python dependencies and daemon +on: + push: + branches: [debian13-python-venv] + pull_request: + branches: [beta] + workflow_dispatch: +permissions: + contents: read +jobs: + dependencies-and-daemon: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + debian: [bookworm, trixie] + container: debian:${{ matrix.debian }} + steps: + - name: Install checkout and daemon prerequisites + run: apt-get update && apt-get install -y git ca-certificates sudo + - uses: actions/checkout@v7 + - name: Install actual BLEA dependencies + run: bash resources/install_apt.sh /tmp/blea-progress + - name: Verify installed Python dependencies + run: resources/python_venv/bin/python3 -c 'import bluepy.btle, bluetooth, requests, serial, pyudev, PIL, numpy, cryptography; from Crypto.Cipher import AES' + - name: Test three daemon socket and SIGTERM cycles without a Bluetooth adapter + run: python3 tests/daemonLifecycle.py diff --git a/core/class/blea.class.php b/core/class/blea.class.php index dd0fcc0..ba2bcf1 100755 --- a/core/class/blea.class.php +++ b/core/class/blea.class.php @@ -321,7 +321,8 @@ public static function launchremote($_remoteId) { $user = $remoteObject->getConfiguration('remoteUser'); $device = $remoteObject->getConfiguration('remoteDevice'); $script_path = '/home/' . $user . '/blead/resources/blead'; - $cmd = '/usr/bin/python3 ' . $script_path . '/blead.py'; + $python = '/home/' . $user . '/blead/resources/python_venv/bin/python3'; + $cmd = 'python=' . escapeshellarg($python) . '; [ -x "$python" ] || python=/usr/bin/python3; "$python" ' . $script_path . '/blead.py'; $cmd .= ' --loglevel ' . log::convertLogLevel(log::getLogLevel('blea')); $cmd .= ' --device ' . $device; $cmd .= ' --socketport ' . config::byKey('socketport', 'blea'); @@ -444,6 +445,13 @@ public static function dependancy_info() { $return['log'] = 'blea_update'; $return['progress_file'] = jeedom::getTmpFolder('blea') . '/dependance'; $return['state'] = 'ok'; + $python = __DIR__ . '/../../resources/python_venv/bin/python3'; + if (is_executable($python)) { + $modules = 'import bluepy.btle, bluetooth, requests, serial, pyudev, PIL, numpy, cryptography; from Crypto.Cipher import AES'; + exec(escapeshellarg($python) . ' -c ' . escapeshellarg($modules) . ' 2>/dev/null', $output, $status); + $return['state'] = ($status === 0) ? 'ok' : 'nok'; + return $return; + } if (exec(system::getCmdSudo() . system::get('cmd_check') . '-E "python3\-serial|python3\-requests|python3\-pyudev|rfkill" | wc -l') < 4) { $return['state'] = 'nok'; } @@ -472,7 +480,11 @@ public static function deamon_start() { } $port = jeedom::getBluetoothMapping(config::byKey('port', 'blea')); $blea_path = realpath(dirname(__FILE__) . '/../../resources/blead'); - $cmd = 'sudo /usr/bin/python3 ' . $blea_path . '/blead.py'; + $python = __DIR__ . '/../../resources/python_venv/bin/python3'; + if (!is_executable($python)) { + $python = '/usr/bin/python3'; + } + $cmd = system::getCmdSudo() . escapeshellarg($python) . ' ' . $blea_path . '/blead.py'; $cmd .= ' --loglevel ' . log::convertLogLevel(log::getLogLevel('blea')); $cmd .= ' --device ' . $port; $cmd .= ' --socketport ' . config::byKey('socketport', 'blea'); diff --git a/resources/install_apt.sh b/resources/install_apt.sh index 56ca733..bab5832 100755 --- a/resources/install_apt.sh +++ b/resources/install_apt.sh @@ -1,3 +1,9 @@ +#!/bin/bash +# Debian 12+ protects the system Python environment (PEP 668). +. /etc/os-release +if [ "${VERSION_ID%%.*}" -ge 12 ]; then + exec bash "$(dirname -- "$0")/install_apt_venv.sh" "$@" +fi PROGRESS_FILE=/tmp/dependancy_blea_in_progress if [ ! -z $1 ]; then PROGRESS_FILE=$1 diff --git a/resources/install_apt_venv.sh b/resources/install_apt_venv.sh new file mode 100644 index 0000000..581f7b4 --- /dev/null +++ b/resources/install_apt_venv.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -Eeuo pipefail +BASE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +PROGRESS_FILE=${1:-/tmp/dependancy_blea_in_progress} +VENV_DIR="$BASE_DIR/python_venv" +trap 'echo "BLEA dependency installation failed at line $LINENO" >&2' ERR +printf '0\n' >"$PROGRESS_FILE" +apt-get update +apt-get install -y python3 python3-dev python3-venv build-essential libglib2.0-dev \ + bluez rfkill python3-bluez python3-requests python3-serial python3-pyudev \ + python3-pil python3-numpy python3-cryptography +printf '40\n' >"$PROGRESS_FILE" +python3 -m venv --system-site-packages "$VENV_DIR" +"$VENV_DIR/bin/python3" -m pip install 'bluepy==1.3.0' pycryptodome +printf '85\n' >"$PROGRESS_FILE" +"$VENV_DIR/bin/python3" -c 'import bluepy.btle, bluetooth, requests, serial, pyudev, PIL, numpy, cryptography; from Crypto.Cipher import AES' +chown -R "$(stat -c '%u:%g' "$BASE_DIR/..")" "$VENV_DIR" +printf '100\n' >"$PROGRESS_FILE" +rm -f -- "$PROGRESS_FILE" +echo 'BLEA dependencies installed in an isolated Python environment' diff --git a/tests/daemonLifecycle.py b/tests/daemonLifecycle.py new file mode 100644 index 0000000..dffee17 --- /dev/null +++ b/tests/daemonLifecycle.py @@ -0,0 +1,29 @@ +from http.server import HTTPServer,BaseHTTPRequestHandler +import threading,subprocess,time,socket,os,tempfile +class H(BaseHTTPRequestHandler): + def do_GET(self):self.send_response(200);self.end_headers();self.wfile.write(b'OK') + def do_POST(self):self.rfile.read(int(self.headers.get('Content-Length','0')));self.do_GET() + def log_message(self,*a):pass +server=HTTPServer(('127.0.0.1',18994),H);threading.Thread(target=server.serve_forever,daemon=True).start() +base=os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'resources') +for cycle in range(3): + pid='/tmp/suite4-blea-'+str(cycle)+'.pid' + with open('/tmp/suite4-blea-'+str(cycle)+'.log','w') as log: + p=subprocess.Popen([base+'/python_venv/bin/python3',base+'/blead/blead.py','--device','hci0','--socketport','18996','--sockethost','127.0.0.1','--callback','http://127.0.0.1:18994/','--apikey','synthetic','--pid',pid,'--daemonname','suite4','--loglevel','debug'],stdout=log,stderr=log) + try: + for i in range(100): + if p.poll() is not None:raise RuntimeError('BLEA stopped before opening socket; see cycle log') + try: + with socket.create_connection(('127.0.0.1',18996),timeout=.1):break + except OSError:time.sleep(.1) + else:raise RuntimeError('No BLEA socket') + print('BLEA_SOCKET_START_PASS',cycle,flush=True) + finally: + p.terminate() + try:p.wait(timeout=10) + except subprocess.TimeoutExpired:p.kill();p.wait();raise + assert p.returncode==0,p.returncode + assert not os.path.exists(pid),pid + print('BLEA_SIGTERM_CLEAN_STOP_PASS',cycle,flush=True) +server.shutdown();server.server_close() +print('NO_BLUETOOTH_RADIO_VALIDATION')