# coding:utf-8
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2026 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
"""Account-side client for the bun delivery socket.
Runs at the account's uid, inside its cage. It says one thing -- "deliver
the modules for this application" -- and reports the verdict. It cannot say
who it is: the kernel tells the daemon that over SO_PEERCRED, which is why
nothing here is worth forging.
Exit codes are the contract with npm_wrapper:
0 delivered
1 declined, for a reason the daemon named (printed to stderr)
2 the daemon is unreachable
3 administrator policy selects classic npm (disabled or excluded)
Only 3 selects classic npm, and only for explicit administrator policy.
Nothing retries after an operational or delivery failure: a silent fallback
is what let two delivery defects sit behind healthy-looking numbers during
this feature's development (see *No fallback* in the design spec).
"""
from __future__ import absolute_import
import json
import os
import re
import socket
import struct
import sys
SOCKET_PATH = '/opt/cl-bun/run/delivery.sock'
# Long enough for a cold cache to fetch a large tree, short enough that a
# wedged daemon does not hold a terminal indefinitely.
# A client can wait in the bounded daemon queue before the Bun install and
# account-side rebuild begin; leave margin for socket and hand-over work too.
TIMEOUT = 2100
_MAJOR = re.compile(r'^[0-9]{1,3}$')
def shell_major(environ=os.environ):
"""The venv the shell is in, from CL_VIRTUAL_ENV, or None.
npm_wrapper sources the venv's own `activate` before any phase runs,
so this names the venv the account is actually working in -- which a
version change may have left beside a newer one the selector records.
The root half has to deliver where the user half resolved and linked,
so the request carries it. Anything but a small number is left unsaid
and the daemon falls back to the record."""
venv = environ.get('CL_VIRTUAL_ENV', '')
name = os.path.basename(venv)
if venv and '/' in venv and _MAJOR.match(name) and 1 <= int(name) <= 999:
return int(name)
return None
def request(command, app_root, path=SOCKET_PATH, timeout=TIMEOUT, major=None,
lock_fd=None):
"""Returns the daemon's verdict as a dict, or raises OSError."""
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect(path)
request = {'cmd': command, 'app_root': app_root}
if major is not None:
request['major'] = major
payload = json.dumps(request)
message = (payload + '\n').encode('utf-8')
if lock_fd is None:
sock.sendall(message)
else:
sent = sock.sendmsg([message], [(socket.SOL_SOCKET, socket.SCM_RIGHTS,
struct.pack('i', lock_fd))])
if sent <= 0:
raise OSError('could not send delivery lock descriptor')
if sent < len(message):
sock.sendall(message[sent:])
chunks = []
while True:
chunk = sock.recv(65536)
if not chunk:
break
chunks.append(chunk)
if b'\n' in chunk:
break
finally:
sock.close()
raw = b''.join(chunks).split(b'\n', 1)[0]
if not raw:
raise OSError('empty reply from delivery service')
return json.loads(raw.decode('utf-8'))
def request_delivery(app_root, path=SOCKET_PATH, timeout=TIMEOUT, major=None,
lock_fd=None):
return request('deliver', app_root, path, timeout, major, lock_fd)
def request_policy(app_root, path=SOCKET_PATH, timeout=TIMEOUT, major=None,
lock_fd=None):
return request('policy', app_root, path, timeout, major, lock_fd)
def request_unshare(app_root, path=SOCKET_PATH, timeout=TIMEOUT, major=None,
lock_fd=None):
return request('unshare', app_root, path, timeout, major, lock_fd)
def main(argv=None):
argv = sys.argv[1:] if argv is None else argv
if len(argv) == 1:
command, app_root = 'deliver', argv[0]
elif len(argv) == 2 and argv[0] in ('policy', 'unshare'):
command, app_root = argv
else:
sys.stderr.write(
'usage: cloudlinux-bun-client [policy|unshare]