diff --git a/README.md b/README.md index 2bd3b1ff..68ec0b7e 100644 --- a/README.md +++ b/README.md @@ -2135,7 +2135,9 @@ buggy feature? rip it out by setting any of the following environment variables | 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_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_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) | @@ -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_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_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_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 | diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 87abc4fc..de9fa7e0 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -64,6 +64,7 @@ from .util import ( expat_ver, gzip, load_ipu, + lock_file, min_ex, mp, odfusion, @@ -419,6 +420,7 @@ class SvcHub(object): # the sessions-db is whatever, if something looks broken then just nuke it db_path = self.args.ses_db + db_lock = db_path + ".lock" try: create = not os.path.getsize(db_path) except: @@ -444,7 +446,11 @@ class SvcHub(object): raise sver = 1 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 except Exception as ex: @@ -460,6 +466,10 @@ class SvcHub(object): db.close() # type: ignore except: pass + try: + os.unlink(db_lock) + except: + pass os.unlink(db_path) def _create_session_db(self, cur: "sqlite3.Cursor") -> None: @@ -476,7 +486,7 @@ class SvcHub(object): cur.execute(cmd) 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) db = cur.connection @@ -488,6 +498,10 @@ class SvcHub(object): except: 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))) if owner: # wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720 @@ -505,6 +519,7 @@ class SvcHub(object): db.commit() cur.close() db.close() + return "" def setup_share_db(self) -> None: al = self.args @@ -513,7 +528,7 @@ class SvcHub(object): al.shr = "" return - import sqlite3 + assert sqlite3 # type: ignore # !rm al.shr = al.shr.strip("/") 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 db_path = self.args.shr_db + db_lock = db_path + ".lock" try: create = not os.path.getsize(db_path) except: @@ -560,6 +576,12 @@ class SvcHub(object): except: 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 = [ 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)", diff --git a/copyparty/util.py b/copyparty/util.py index 5cd16b5f..7849f103 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -114,8 +114,14 @@ IP6ALL = "0:0:0:0:0:0:0:0" try: - import ctypes import fcntl + + HAVE_FCNTL = True +except: + HAVE_FCNTL = False + +try: + import ctypes import termios except: pass @@ -3940,6 +3946,73 @@ def hidedir(dp) -> None: 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: if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"): # py3.8 doesn't have .files