#!/usr/bin/env python3 """Atomically copy one dropped image without replacing an existing file.""" import argparse import ctypes import os import select import signal import stat import sys import tempfile import time import urllib.parse TEMP_PREFIX = ".dropwall-copy-" TEMP_SUFFIX = ".tmp" STALE_SECONDS = 24 * 60 * 60 ACTIVE_TEMP = None COPY_CHUNK = 1024 * 1024 # Exit immediately if Noctalia closes the process pipe. signal.signal(signal.SIGPIPE, signal.SIG_DFL) def arm_parent_death_signal(): """Ask Linux to terminate this copy if Noctalia disappears.""" parent = os.getppid() try: libc = ctypes.CDLL(None, use_errno=True) if libc.prctl(1, signal.SIGTERM, 0, 0, 0) != 0: # PR_SET_PDEATHSIG return if os.getppid() != parent: os.kill(os.getpid(), signal.SIGTERM) except (AttributeError, OSError): return def encode_path(path): return urllib.parse.quote_from_bytes(os.fsencode(path), safe="") def remove_active_temp(): global ACTIVE_TEMP if ACTIVE_TEMP: try: os.unlink(ACTIVE_TEMP) except OSError: pass ACTIVE_TEMP = None def terminate(signum, _frame): remove_active_temp() os._exit(128 + signum) class WorkerLease: """A pidfd tied to the GTK worker that accepted this drop.""" def __init__(self, pid): if not hasattr(os, "pidfd_open"): raise OSError("this Linux/Python build does not support pidfd_open") self.fd = os.pidfd_open(pid, 0) self.poller = select.poll() self.poller.register(self.fd, select.POLLIN | select.POLLHUP | select.POLLERR) self.check() def check(self): if self.poller.poll(0): raise BrokenPipeError("DropWall worker stopped during the copy") def close(self): os.close(self.fd) def cleanup_stale_temps(directory): """Remove only old, owned temporary files left by interrupted copies.""" cutoff = time.time() - STALE_SECONDS try: names = os.listdir(directory) except OSError: return for name in names: if not (name.startswith(TEMP_PREFIX) and name.endswith(TEMP_SUFFIX)): continue path = os.path.join(directory, name) try: info = os.lstat(path) if info.st_uid == os.getuid() and stat.S_ISREG(info.st_mode) and info.st_mtime < cutoff: os.unlink(path) except OSError: continue def is_inside(path, directory): try: return os.path.commonpath((path, directory)) == directory except ValueError: return False def publish_unique(temp_path, source, directory, lease): filename = os.path.basename(source) stem, suffix = os.path.splitext(filename) stem = stem or "wallpaper" for counter in range(10000): candidate_name = filename if counter == 0 else "%s-%d%s" % (stem, counter, suffix) candidate = os.path.join(directory, candidate_name) try: lease.check() # The hard link exposes the already-complete inode atomically and # fails if candidate exists. It can never replace user data. os.link(temp_path, candidate, follow_symlinks=False) return candidate except FileExistsError: continue raise FileExistsError("could not allocate a unique destination filename") def copy_atomic(source, directory, lease): global ACTIVE_TEMP real_directory = os.path.realpath(directory) if not os.path.isdir(real_directory): raise NotADirectoryError("wallpaper directory does not exist") source_flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NONBLOCK", 0) source_fd = os.open(source, source_flags) with os.fdopen(source_fd, "rb") as source_file: source_info = os.fstat(source_file.fileno()) if not stat.S_ISREG(source_info.st_mode): raise OSError("the dropped path is not a regular file") # Resolve the descriptor we actually validated, not a pathname that # could have been swapped after open(). real_source = os.path.realpath("/proc/self/fd/%d" % source_file.fileno()) if is_inside(real_source, real_directory): return real_source cleanup_stale_temps(real_directory) temp_fd, temp_path = tempfile.mkstemp( prefix=TEMP_PREFIX, suffix=TEMP_SUFFIX, dir=real_directory, ) ACTIVE_TEMP = temp_path try: with os.fdopen(temp_fd, "wb") as temp_file: while True: lease.check() chunk = source_file.read(COPY_CHUNK) if not chunk: break temp_file.write(chunk) temp_file.flush() os.fsync(temp_file.fileno()) # Keep copies private even if the source was more permissive. os.chmod(temp_path, 0o600) return publish_unique(temp_path, source, real_directory, lease) finally: remove_active_temp() def main(): arm_parent_death_signal() parser = argparse.ArgumentParser(description="Safely copy one DropWall image") parser.add_argument("--lease-pid", type=int, required=True) parser.add_argument("source") parser.add_argument("directory") args = parser.parse_args() for signum in (signal.SIGINT, signal.SIGTERM): signal.signal(signum, terminate) lease = None try: lease = WorkerLease(args.lease_pid) destination = copy_atomic(args.source, args.directory, lease) except Exception as error: print(str(error).replace("\n", " "), file=sys.stderr, flush=True) return 1 finally: if lease is not None: lease.close() print("COPIED\t%s" % encode_path(destination), flush=True) return 0 if __name__ == "__main__": sys.exit(main())