Selkies
Developer Referenceprinting

PrintQueue

The session's print queue: a CUPS scheduler run as this user under the runtime directory, with one queue, Selkies, whose backend writes each job into the spool. The scheduler's programs are the installed ones, reached through a private tree that adds the backend, and its whole state lives under root, so nothing under /etc/cups is touched and no privilege is needed. Applications reach it with CUPS_SERVER set to socket.

Attributes

attributeroot
= os.path.join(runtime, 'selkies-cups') if runtime else os.path.join(tempfile.gettempdir(), f'selkies-cups-{os.getuid()}')
attributesocket
= os.path.join(self.root, 'cups.sock')
attributespool
= spool
attributeprocessOptional[asyncio.subprocess.Process]
= None

Functions

constructor__init__(spool) -> None
Source Code
def __init__(self, spool: str) -> None:
    runtime = os.environ.get("XDG_RUNTIME_DIR")
    self.root = os.path.join(runtime, "selkies-cups") if runtime else \
        os.path.join(tempfile.gettempdir(), f"selkies-cups-{os.getuid()}")
    self.socket = os.path.join(self.root, "cups.sock")
    self.spool = spool
    self.process: Optional[asyncio.subprocess.Process] = None
paramspoolstr

Returns

None
funcprograms() -> Optional[Tuple[str, str, str]]

(cupsd, server bin, data dir) of the installed CUPS, or None.

Source Code
@staticmethod
def programs() -> Optional[Tuple[str, str, str]]:
    """`(cupsd, server bin, data dir)` of the installed CUPS, or None."""
    path = os.environ.get("PATH", "") + ":/usr/sbin:/usr/local/sbin"
    cupsd = shutil.which("cupsd", path=path)
    if not cupsd:
        return None
    prefix = os.path.dirname(os.path.dirname(os.path.realpath(cupsd)))
    for lib in ("lib", "lib64", "libexec"):
        server_bin = os.path.join(prefix, lib, "cups")
        if os.path.isfile(os.path.join(server_bin, "daemon", "cups-exec")) \
                and os.path.isdir(os.path.join(server_bin, "filter")):
            return cupsd, server_bin, os.path.join(prefix, "share", "cups")
    return None

Returns

typing.Optional[typing.Tuple[str, str, str]]
func_prepare(server_bin, data_dir) -> None
Source Code
    def _prepare(self, server_bin: str, data_dir: str) -> None:
        for sub in ("ppd", "state", "cache", "spool", "tmp", "bin/backend"):
            os.makedirs(os.path.join(self.root, sub), exist_ok=True)
        os.makedirs(self.spool, exist_ok=True)
        for sub in ("filter", "daemon"):
            link = os.path.join(self.root, "bin", sub)
            if os.path.islink(link):
                os.unlink(link)
            os.symlink(os.path.join(server_bin, sub), link)
        package = files("selkies") / "cups"
        backend = os.path.join(self.root, "bin", "backend", "selkies")
        with open(backend, "wb") as out:
            out.write((package / "backend").read_bytes())
        os.chmod(backend, 0o755)
        with open(os.path.join(self.root, "ppd", "Selkies.ppd"), "wb") as out:
            out.write((package / "selkies.ppd").read_bytes())
        confs = {
            "cups-files.conf": f"""ServerRoot {self.root}
ServerBin {os.path.join(self.root, "bin")}
DataDir {data_dir}
StateDir {os.path.join(self.root, "state")}
CacheDir {os.path.join(self.root, "cache")}
RequestRoot {os.path.join(self.root, "spool")}
TempDir {os.path.join(self.root, "tmp")}
LogFileGroup {os.getgid()}
AccessLog {os.path.join(self.root, "access.log")}
PageLog {os.path.join(self.root, "page.log")}
ErrorLog {os.path.join(self.root, "error.log")}
""",
            "cupsd.conf": f"""Listen {self.socket}
LogLevel warn
Browsing Off
WebInterface No
""",
            "printers.conf": f"""<DefaultPrinter Selkies>
Info Selkies printer
Location In the browser
DeviceURI selkies:{self.spool}
State Idle
Accepting Yes
Shared No
JobSheets none none
ErrorPolicy retry-job
</DefaultPrinter>
""",
        }
        for name, text in confs.items():
            with open(os.path.join(self.root, name), "w") as out:
                out.write(text)
        if os.path.exists(self.socket):
            os.unlink(self.socket)
paramserver_binstr
paramdata_dirstr

Returns

None
funcstart() -> bool

Start the scheduler; False where CUPS is not installed.

Source Code
async def start(self) -> bool:
    """Start the scheduler; False where CUPS is not installed."""
    found = self.programs()
    if found is None:
        logger.info("No CUPS scheduler on this host: documents printed into the spool are "
                    "still handed over, but there is no Selkies queue to print to "
                    "(cups-daemon and cups-filters provide one)")
        return False
    cupsd, server_bin, data_dir = found
    # The scheduler runs as a copy of the program: a distribution confines
    # the system scheduler, by its path, to the system's directories and
    # backends, which a queue under the runtime directory has neither of.
    # The copy is renamed into place, which a scheduler still running the
    # old copy (left by a server that died without stopping it) never
    # blocks the way writing over its program would.
    program = os.path.join(self.root, "cupsd")
    try:
        self._prepare(server_bin, data_dir)
        shutil.copy2(cupsd, program + ".new")
        os.replace(program + ".new", program)
    except OSError as exc:
        logger.warning("No print queue: cannot set the scheduler up under %s: %s", self.root, exc)
        return False
    args = ["-f", "-c", os.path.join(self.root, "cupsd.conf"), "-s", os.path.join(self.root, "cups-files.conf")]
    spawn = dict(stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
                 preexec_fn=_die_with_parent)
    try:
        self.process = await asyncio.create_subprocess_exec(program, *args, **spawn)
    except OSError:
        self.process = await asyncio.create_subprocess_exec(cupsd, *args, **spawn)
    for _ in range(100):
        if os.path.exists(self.socket) or self.process.returncode is not None:
            break
        await asyncio.sleep(0.1)
    if not os.path.exists(self.socket):
        logger.warning("The print queue did not come up; its log is %s",
                       os.path.join(self.root, "error.log"))
        await self.stop()
        return False
    logger.info("Print queue Selkies listening on %s, spooling to %s", self.socket, self.spool)
    return True

Returns

bool
funcstop() -> None
Source Code
async def stop(self) -> None:
    process, self.process = self.process, None
    if process is None or process.returncode is not None:
        return
    process.terminate()
    try:
        await asyncio.wait_for(process.wait(), 5)
    except asyncio.TimeoutError:
        process.kill()
        await process.wait()

Returns

None

On this page

Edit on GitHub