enforce single-instance for session/shares db

use file-locking to detect and prevent misconfigurations
which could lead to subtle unexpected behavior
This commit is contained in:
ed 2025-04-08 19:08:12 +00:00
parent 8e0364efad
commit acfaacbd46
3 changed files with 101 additions and 5 deletions

View file

@ -2135,7 +2135,9 @@ buggy feature? rip it out by setting any of the following environment variables
| env-var | what it does | | env-var | what it does |
| -------------------- | ------------ | | -------------------- | ------------ |
| `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access |
| `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes | | `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes |
| `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` |
| `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) | | `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) |
| `PRTY_NO_LZMA` | disable streaming xz compression of incoming uploads | | `PRTY_NO_LZMA` | disable streaming xz compression of incoming uploads |
| `PRTY_NO_MP` | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) | | `PRTY_NO_MP` | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) |
@ -2676,7 +2678,6 @@ set any of the following environment variables to disable its associated optiona
| `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) | | `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) |
| `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips | | `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips |
| `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen | | `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen |
| `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` |
| `PRTY_NO_MUTAGEN` | do not use [mutagen](https://pypi.org/project/mutagen/) for reading metadata from media files; will fallback to ffprobe | | `PRTY_NO_MUTAGEN` | do not use [mutagen](https://pypi.org/project/mutagen/) for reading metadata from media files; will fallback to ffprobe |
| `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg | | `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg |
| `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails | | `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails |

View file

@ -64,6 +64,7 @@ from .util import (
expat_ver, expat_ver,
gzip, gzip,
load_ipu, load_ipu,
lock_file,
min_ex, min_ex,
mp, mp,
odfusion, odfusion,
@ -419,6 +420,7 @@ class SvcHub(object):
# the sessions-db is whatever, if something looks broken then just nuke it # the sessions-db is whatever, if something looks broken then just nuke it
db_path = self.args.ses_db db_path = self.args.ses_db
db_lock = db_path + ".lock"
try: try:
create = not os.path.getsize(db_path) create = not os.path.getsize(db_path)
except: except:
@ -444,7 +446,11 @@ class SvcHub(object):
raise raise
sver = 1 sver = 1
self._create_session_db(cur) self._create_session_db(cur)
self._verify_session_db(cur, sver) err = self._verify_session_db(cur, sver, db_path)
if err:
tries = 99
self.args.no_ses = True
self.log("root", err, 3)
break break
except Exception as ex: except Exception as ex:
@ -460,6 +466,10 @@ class SvcHub(object):
db.close() # type: ignore db.close() # type: ignore
except: except:
pass pass
try:
os.unlink(db_lock)
except:
pass
os.unlink(db_path) os.unlink(db_path)
def _create_session_db(self, cur: "sqlite3.Cursor") -> None: def _create_session_db(self, cur: "sqlite3.Cursor") -> None:
@ -476,7 +486,7 @@ class SvcHub(object):
cur.execute(cmd) cur.execute(cmd)
self.log("root", "created new sessions-db") self.log("root", "created new sessions-db")
def _verify_session_db(self, cur: "sqlite3.Cursor", sver: int) -> None: def _verify_session_db(self, cur: "sqlite3.Cursor", sver: int, db_path: str) -> str:
# ensure writable (maybe owned by other user) # ensure writable (maybe owned by other user)
db = cur.connection db = cur.connection
@ -488,6 +498,10 @@ class SvcHub(object):
except: except:
owner = 0 owner = 0
if not lock_file(db_path + ".lock"):
t = "the sessions-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --ses-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now disable sessions and instead use plaintext passwords in cookies."
return t % (db_path, owner)
vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000))) vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000)))
if owner: if owner:
# wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720 # wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720
@ -505,6 +519,7 @@ class SvcHub(object):
db.commit() db.commit()
cur.close() cur.close()
db.close() db.close()
return ""
def setup_share_db(self) -> None: def setup_share_db(self) -> None:
al = self.args al = self.args
@ -513,7 +528,7 @@ class SvcHub(object):
al.shr = "" al.shr = ""
return return
import sqlite3 assert sqlite3 # type: ignore # !rm
al.shr = al.shr.strip("/") al.shr = al.shr.strip("/")
if "/" in al.shr or not al.shr: if "/" in al.shr or not al.shr:
@ -528,6 +543,7 @@ class SvcHub(object):
# the shares-db is important, so panic if something is wrong # the shares-db is important, so panic if something is wrong
db_path = self.args.shr_db db_path = self.args.shr_db
db_lock = db_path + ".lock"
try: try:
create = not os.path.getsize(db_path) create = not os.path.getsize(db_path)
except: except:
@ -560,6 +576,12 @@ class SvcHub(object):
except: except:
owner = 0 owner = 0
if not lock_file(db_lock):
t = "the shares-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --shr-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now panic."
t = t % (db_path, owner)
self.log("root", t, 1)
raise Exception(t)
sch1 = [ sch1 = [
r"create table kv (k text, v int)", r"create table kv (k text, v int)",
r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)", r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)",

View file

@ -114,8 +114,14 @@ IP6ALL = "0:0:0:0:0:0:0:0"
try: try:
import ctypes
import fcntl import fcntl
HAVE_FCNTL = True
except:
HAVE_FCNTL = False
try:
import ctypes
import termios import termios
except: except:
pass pass
@ -3940,6 +3946,73 @@ def hidedir(dp) -> None:
pass pass
_flocks = {}
def _lock_file_noop(ap: str) -> bool:
return True
def _lock_file_ioctl(ap: str) -> bool:
assert fcntl # type: ignore # !rm
try:
fd = _flocks.pop(ap)
os.close(fd)
except:
pass
fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438)
# NOTE: the fcntl.lockf identifier is (pid,node);
# the lock will be dropped if os.close(os.open(ap))
# is performed anywhere else in this thread
try:
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
_flocks[ap] = fd
return True
except Exception as ex:
eno = getattr(ex, "errno", -1)
try:
os.close(fd)
except:
pass
if eno in (errno.EAGAIN, errno.EACCES):
return False
print("WARNING: unexpected errno %d from fcntl.lockf; %r" % (eno, ex))
return True
def _lock_file_windows(ap: str) -> bool:
try:
import msvcrt
try:
fd = _flocks.pop(ap)
os.close(fd)
except:
pass
fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438)
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
return True
except Exception as ex:
eno = getattr(ex, "errno", -1)
if eno == errno.EACCES:
return False
print("WARNING: unexpected errno %d from msvcrt.locking; %r" % (eno, ex))
return True
if os.environ.get("PRTY_NO_DB_LOCK"):
lock_file = _lock_file_noop
elif ANYWIN:
lock_file = _lock_file_windows
elif HAVE_FCNTL:
lock_file = _lock_file_ioctl
else:
lock_file = _lock_file_noop
try: try:
if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"): if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"):
# py3.8 doesn't have .files # py3.8 doesn't have .files