Files
lbgos--rift-s-linux/scripts/rifts-setup.in
T
lbgos f046863394 Re-establish registration watches after Steam moves
inotify drops watches on moved or deleted directories and wait()
drains the fd without parsing, so the remembered path set kept dead
entries forever. Re-add watches every pass so watch mode recovers
after a Steam reinstall instead of stalling until service restart.

(cherry picked from commit 9cd08b0d7a4cb095f6bae516f42d141e1d2b0d64)
2026-10-13 00:31:49 +03:00

172 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""Register the installed Rift S driver using SteamVR's own path registry tool."""
import argparse
import ctypes
import fcntl
import json
import os
from pathlib import Path
import re
import select
import subprocess
import sys
import time
DRIVER = Path('@CMAKE_INSTALL_FULL_DATADIR@/steamvr-monado')
def read_registry(path):
try:
value = json.loads(path.read_text())
return value if isinstance(value, dict) else {}
except (FileNotFoundError, json.JSONDecodeError):
return {}
def steam_roots(home):
roots = [home / '.local/share/Steam', home / '.steam/steam', home / '.steam/root']
result = []
for root in roots:
root = root.resolve()
if root.is_dir() and root not in result:
result.append(root)
return result
def find_runtime(home, registry):
roots = steam_roots(home)
libraries = list(roots)
for root in roots:
folders = root / 'steamapps/libraryfolders.vdf'
if folders.is_file():
# Only library paths are needed; never read account or login files.
for path in re.findall(r'"path"\s*"((?:\\.|[^"\\])*)"', folders.read_text()):
library = Path(path.replace('\\\\', '\\').replace('\\"', '"'))
if library not in libraries:
libraries.append(library)
candidates = [Path(path) for path in registry.get('runtime', []) if isinstance(path, str)]
candidates += [path / 'steamapps/common/SteamVR' for path in libraries]
for runtime in candidates:
tool = runtime / 'bin/linux64/vrpathreg'
if tool.is_file() and os.access(tool, os.X_OK):
return tool, roots, libraries
return None, roots, libraries
def pathreg(tool, *arguments):
# Native vrpathreg can report paths. Do not copy those or subprocess environment to logs.
result = subprocess.run([str(tool), *arguments], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=20)
if result.returncode:
raise RuntimeError('SteamVR driver registration failed; will retry.')
def register(home, registry_path, uninstall=False):
registry = read_registry(registry_path)
tool, roots, libraries = find_runtime(home, registry)
if tool is None:
return False, roots, libraries
drivers = registry.get('external_drivers') or []
if uninstall:
if str(DRIVER) in drivers:
pathreg(tool, 'removedriver', str(DRIVER))
return True, roots, libraries
if not (DRIVER / 'driver.vrdrivermanifest').is_file():
raise RuntimeError('Installed Rift S driver manifest is missing.')
if str(DRIVER) in drivers:
return True, roots, libraries
# Bootstrap only missing native paths. Existing runtime/config/log paths remain intact.
if not registry.get('runtime'):
pathreg(tool, 'setthis')
root = roots[0] if roots else home / '.local/share/Steam'
if not registry.get('config'):
pathreg(tool, 'setconfig', str(root / 'config'))
if not registry.get('log'):
pathreg(tool, 'setlog', str(root / 'logs'))
pathreg(tool, 'adddriver', str(DRIVER))
return True, roots, libraries
class DirectoryEvents:
def __init__(self):
self.libc = ctypes.CDLL(None, use_errno=True)
self.libc.inotify_init1.argtypes = [ctypes.c_int]
self.libc.inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32]
self.fd = self.libc.inotify_init1(os.O_NONBLOCK | os.O_CLOEXEC)
self.watches = set()
def watch(self, directories):
if self.fd < 0:
return
for directory in directories:
directory = directory.resolve()
while not directory.is_dir() and directory != directory.parent:
directory = directory.parent
# Re-add every pass: the kernel drops watches on moved or deleted
# directories (IN_IGNORED), which wait() drains unread. Skipping
# known paths here would leave watch mode deaf after a Steam
# reinstall until the service restarts.
mask = 0x100 | 0x80 | 0x8 | 0x400 | 0x800
if self.libc.inotify_add_watch(self.fd, os.fsencode(directory), mask) >= 0:
self.watches.add(directory)
def wait(self):
if self.fd < 0:
time.sleep(2)
elif select.select([self.fd], [], [], 2)[0]:
try:
os.read(self.fd, 65536)
except BlockingIOError:
pass
time.sleep(0.1)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('action', choices=['install', 'uninstall', 'watch'], nargs='?', default='install')
args = parser.parse_args()
if os.geteuid() == 0:
parser.error('Run in the Steam user session.')
home = Path.home()
config = Path(os.environ.get('XDG_CONFIG_HOME', home / '.config'))
registry_path = Path(os.environ.get('VR_PATHREG_OVERRIDE', config / 'openvr/openvrpaths.vrpath'))
state = Path(os.environ.get('XDG_STATE_HOME', home / '.local/state')) / 'monado-rifts'
state.mkdir(parents=True, exist_ok=True, mode=0o700)
events = DirectoryEvents()
last_error = None
with (state / 'registration.lock').open('w') as lock:
while True:
with_lock = False
try:
fcntl.flock(lock, fcntl.LOCK_EX)
with_lock = True
done, roots, libraries = register(home, registry_path, args.action == 'uninstall')
last_error = None
except (OSError, RuntimeError, subprocess.TimeoutExpired) as error:
if args.action != 'watch':
raise SystemExit(str(error))
if last_error != str(error):
print(str(error), file=sys.stderr, flush=True)
last_error = str(error)
roots, libraries = steam_roots(home), []
done = False
finally:
if with_lock:
fcntl.flock(lock, fcntl.LOCK_UN)
if args.action != 'watch':
print('Rift S registered.' if done else 'Rift S registration will complete when SteamVR is installed.')
return
directories = [config / 'openvr', home / '.local/share/Steam', home / '.steam']
for root in roots:
directories.extend([root, root / 'steamapps'])
for library in libraries:
runtime = library / 'steamapps/common/SteamVR'
directories.extend([library, library / 'steamapps', library / 'steamapps/common',
runtime, runtime / 'bin', runtime / 'bin/linux64'])
events.watch(directories)
events.wait()
if __name__ == '__main__':
main()