Use shared locks for Fenrir instance coordination

This commit is contained in:
Storm Dragon
2026-05-08 00:01:58 -04:00
parent b38b0a2dab
commit a60efdbe07
3 changed files with 72 additions and 65 deletions
@@ -5,6 +5,7 @@
# By Chrys, Storm Dragon, and contributors.
import datetime
import fcntl
import os
import tempfile
import time
@@ -34,55 +35,45 @@ class command:
def _get_announcement_lock_path(self):
return os.path.join(
tempfile.gettempdir(),
f"fenrirscreenreader-{os.getuid()}-time-announcement.lock",
"fenrirscreenreader-time-announcement.lock",
)
def _try_create_announcement_lock(self, announcement_slot, now):
lock_path = self._get_announcement_lock_path()
try:
lock_fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError:
return False
with os.fdopen(lock_fd, "w", encoding="utf-8") as lock_file:
lock_file.write(f"{os.getpid()} {announcement_slot} {now}\n")
return True
def _read_announcement_lock_slot(self, lock_path):
with open(lock_path, "r", encoding="utf-8") as lock_file:
lock_content = lock_file.readline().strip().split()
def _read_announcement_lock_slot(self, lock_file):
lock_file.seek(0)
lock_content = lock_file.readline().strip().split()
if len(lock_content) < 2:
return ""
return lock_content[1]
def _claim_announcement_lock(self, announcement_slot):
now = time.time()
if self._try_create_announcement_lock(announcement_slot, now):
return True
lock_path = self._get_announcement_lock_path()
try:
lock_slot = self._read_announcement_lock_slot(lock_path)
lock_stat = os.stat(lock_path)
except FileNotFoundError:
return self._try_create_announcement_lock(announcement_slot, now)
lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o666)
except OSError:
return False
if lock_slot == announcement_slot:
return False
if not lock_slot and now - lock_stat.st_mtime < ANNOUNCEMENT_LOCK_TIMEOUT_SEC:
return False
try:
os.unlink(lock_path)
except FileNotFoundError:
pass
os.chmod(lock_path, 0o666)
except OSError:
return False
pass
return self._try_create_announcement_lock(announcement_slot, now)
with os.fdopen(lock_fd, "r+", encoding="utf-8") as lock_file:
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
lock_slot = self._read_announcement_lock_slot(lock_file)
lock_stat = os.fstat(lock_file.fileno())
if lock_slot == announcement_slot:
return False
lock_file.seek(0)
lock_file.truncate()
lock_file.write(f"{os.getpid()} {announcement_slot} {now}\n")
lock_file.flush()
os.fsync(lock_file.fileno())
return True
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
+46 -30
View File
@@ -27,6 +27,7 @@ command interrupt
import hashlib
import fcntl
import os
import tempfile
import time
@@ -38,7 +39,7 @@ from fenrirscreenreader.core.i18n import _
REMOTE_COMMAND_LOCK_TIMEOUT_SEC = 2.0
REMOTE_COMMAND_LOCK_PREFIX = f"fenrirscreenreader-{os.getuid()}-remote-"
REMOTE_COMMAND_LOCK_PREFIX = "fenrirscreenreader-remote-"
class RemoteManager:
@@ -304,47 +305,62 @@ class RemoteManager:
except OSError:
pass
def _try_create_remote_command_lock(self, lock_path, now):
def _read_remote_command_lock(self, lock_file):
lock_file.seek(0)
lock_parts = lock_file.readline().strip().split()
try:
lock_fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError:
return False
lock_pid = int(lock_parts[0]) if lock_parts else 0
except ValueError:
lock_pid = 0
return lock_pid
with os.fdopen(lock_fd, "w", encoding="utf-8") as lock_file:
lock_file.write(f"{os.getpid()} {now}\n")
return True
def _write_remote_command_lock(self, lock_file, now):
lock_file.seek(0)
lock_file.truncate()
lock_file.write(f"{os.getpid()} {now}\n")
lock_file.flush()
os.fsync(lock_file.fileno())
def _open_remote_command_lock(self, lock_path):
try:
lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o666)
except OSError:
return None
try:
os.chmod(lock_path, 0o666)
except OSError:
pass
return os.fdopen(lock_fd, "r+", encoding="utf-8")
def _claim_remote_command(self, event_data):
lock_path = self._get_remote_command_lock_path(event_data)
now = time.time()
self._cleanup_stale_remote_command_locks(now)
if self._try_create_remote_command_lock(lock_path, now):
return True
try:
with open(lock_path, "r", encoding="utf-8") as lock_file:
lock_parts = lock_file.readline().strip().split()
lock_pid = int(lock_parts[0]) if lock_parts else 0
lock_stat = os.stat(lock_path)
except FileNotFoundError:
return self._try_create_remote_command_lock(lock_path, now)
except (OSError, ValueError):
lock_file = self._open_remote_command_lock(lock_path)
if lock_file is None:
return False
if lock_pid == os.getpid():
return True
with lock_file:
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
lock_pid = self._read_remote_command_lock(lock_file)
lock_stat = os.fstat(lock_file.fileno())
if now - lock_stat.st_mtime < REMOTE_COMMAND_LOCK_TIMEOUT_SEC:
return False
if lock_pid == os.getpid():
return True
try:
os.unlink(lock_path)
except FileNotFoundError:
pass
except OSError:
return False
if not lock_pid:
self._write_remote_command_lock(lock_file, now)
return True
return self._try_create_remote_command_lock(lock_path, now)
if now - lock_stat.st_mtime < REMOTE_COMMAND_LOCK_TIMEOUT_SEC:
return False
self._write_remote_command_lock(lock_file, now)
return True
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
def list_instances(self):
instances = remoteInstanceRegistry.list_instances()
+1 -1
View File
@@ -4,5 +4,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
version = "2026.05.07"
version = "2026.05.08"
code_name = "testing"