| Current Path : /home2/noxxprod/www/c52d7/ |
| Current File : //home2/noxxprod/www/c52d7/l.v.e-manager.tar |
cl.nodejs/yum_cache.dat 0000644 00000000214 15272570413 0011055 0 ustar 00 alt-nodejs6
alt-nodejs8
alt-nodejs9
alt-nodejs10
alt-nodejs11
alt-nodejs12
alt-nodejs14
alt-nodejs16
alt-nodejs18
alt-nodejs19
alt-nodejs20
cl.nodejs/selector.json 0000644 00000000075 15272570413 0011146 0 ustar 00 {
"default_version": "20",
"selector_enabled": true
} utils/activate 0000755 00000004461 15272570413 0007445 0 ustar 00 #!/bin/bash
if [[ x"${BASH_SOURCE[0]}" == x"$0" ]]; then
echo "'activate' script should be sourced, not run directly"
exit 1;
fi
CWD=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
NEW_VIRTUAL_ENV_PATH="${CWD%/bin}"
deactivate () {
if [[ x"${BASH_SOURCE[0]}" == x"$0" ]]; then
echo "'deactivate' script should be sourced, not run directly"
exit 1;
fi
# Only restore from backup variables if they are set
# But include the case when they are set to be empty
if [[ ${BKP_PATH+"is_set"} == "is_set" ]]; then
PATH="$BKP_PATH"
export PATH
fi
if [[ ${BKP_NODE_PATH+"is_set"} == "is_set" ]]; then
NODE_PATH="$BKP_NODE_PATH"
export NODE_PATH
fi
if [[ ! -z $BKP_PS1 ]]; then
PS1="$BKP_PS1"
export PS1
fi
unset -v BKP_PATH
unset -v BKP_NODE_PATH
unset -v BKP_PS1
unset -v CL_VIRTUAL_ENV
unset -v CL_APP_ROOT
unset -v CL_NODEHOME
unset -v CL_NODEJS_VERSION
if [ ! "${1-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
activate () {
CL_VIRTUAL_ENV="${NEW_VIRTUAL_ENV_PATH}"
CL_NODEJS_VERSION="$(echo "${CL_VIRTUAL_ENV}" | awk -F '/' '{print $NF}')"
CL_APP_ROOT="${CL_VIRTUAL_ENV#$HOME/nodevenv/}" # cut $HOME/nodevenv/
CL_APP_ROOT="${CL_APP_ROOT%/$CL_NODEJS_VERSION}" # cut nodejs version
CL_NODEHOME="/opt/alt/alt-nodejs${CL_NODEJS_VERSION}/root"
BKP_NODE_PATH="$NODE_PATH"
NODE_PATH="$CL_VIRTUAL_ENV/lib/node_modules:$CL_NODEHOME/lib/node_modules:$CL_NODEHOME/lib64/node_modules:$NODE_PATH"
BKP_PATH="$PATH"
PATH="$CL_VIRTUAL_ENV/bin:$CL_NODEHOME/usr/bin:$CL_VIRTUAL_ENV/lib/bin/:$PATH"
if [[ -z "$CL_VIRTUAL_ENV_DISABLE_PROMPT" ]] ; then
BKP_PS1="$PS1"
PS1="[${CL_APP_ROOT} ($CL_NODEJS_VERSION)] $PS1"
fi
export BKP_PS1
export BKP_PATH
export BKP_NODE_PATH
export PS1
export CL_VIRTUAL_ENV
export CL_APP_ROOT
export CL_NODEHOME
export NODE_PATH
export PATH
}
# compare current virtual environment (that is stored in CL_VIRTUAL_ENV) path
# to the NEW_VIRTUAL_ENV_PATH, that is the path of the new environment we may enter too
# just do nothing if paths are equal
if [ "${CL_VIRTUAL_ENV}" != "${NEW_VIRTUAL_ENV_PATH}" ]; then
deactivate nondestructive
activate
fi
utils/cloudlinux_bun_client.py 0000755 00000017575 15272570413 0012676 0 ustar 00 # 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.
The account-side Selector also uses it once, after a Selector install it
drove failed its restart/health check: `unhealthy` reports that, through
the same application lock, so the daemon can count it as broken.
Exit codes are the contract with npm_wrapper:
0 delivered (for `unhealthy`: recorded)
1 declined, for a reason the daemon named (printed to stderr)
2 the daemon is unreachable
3 classic npm: administrator policy (disabled or excluded), or, on
`deliver`, a dependency source the Store does not serve
(unsupported_source, registry_not_allowed), an engine limit
(lockfile_too_large, too_many_packages) or an `npm ci` it cannot
install exactly (lockfile_mismatch)
Only 3 selects classic npm, and only for those reasons. For a classic
`deliver` fallback the reason is the one line printed on stdout. 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, via=None, kind=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
if via is not None:
request['via'] = via
if kind is not None:
request['kind'] = kind
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, via=None, kind=None):
return request('deliver', app_root, path, timeout, major, lock_fd, via,
kind)
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 request_unhealthy(app_root, path=SOCKET_PATH, timeout=TIMEOUT, major=None,
lock_fd=None):
return request('unhealthy', 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', 'unhealthy'):
command, app_root = argv
else:
sys.stderr.write('usage: cloudlinux-bun-client '
'[policy|unshare|unhealthy] <app-root>\n')
return 2
try:
raw_lock_fd = os.environ.get('CL_BUN_LOCK_FD')
lock_fd = int(raw_lock_fd) if raw_lock_fd and raw_lock_fd.isdigit() else None
request_fn = {
'deliver': request_delivery,
'policy': request_policy,
'unshare': request_unshare,
'unhealthy': request_unhealthy,
}[command]
kwargs = {'major': shell_major(), 'lock_fd': lock_fd}
# npm_wrapper sets this only after validating the Selector's app lock
# descriptor: the install is Selector-driven, counted as `panel`.
if command == 'deliver' and os.environ.get('CL_BUN_VIA') == 'selector':
kwargs['via'] = 'selector'
# npm_wrapper sets this for a bare `npm ci` it routes to the Store.
if command == 'deliver' and os.environ.get('CL_BUN_COMMAND') == 'ci':
kwargs['kind'] = 'ci'
verdict = request_fn(app_root, **kwargs)
except (OSError, ValueError) as exc:
sys.stderr.write('bun delivery unavailable: %s\n' % exc)
return 2
if verdict.get('status') == 'delivered':
return 0
if verdict.get('status') == 'unshared':
return 0
if command == 'unhealthy' and verdict.get('status') == 'recorded':
return 0
if command == 'policy' and verdict.get('status') == 'eligible':
return 0
if (command in ('deliver', 'policy')
and verdict.get('reason') in ('disabled', 'excluded')):
# These two are explicit administrator choices of classic npm. The
# wrapper distinguishes them from every actual delivery failure.
return 3
if (command == 'deliver' and verdict.get('reason')
in ('unsupported_source', 'registry_not_allowed',
'lockfile_mismatch', 'lockfile_too_large',
'too_many_packages')):
# Engine incapability, like a resolve sentinel: root refused a source
# or package shape the Store does not serve, or an `npm ci` it cannot
# install exactly, logged it and named the application in its
# fallback registry for `status`. The account is not told; the
# wrapper takes exit 3 to its classic path.
# The one line stdout ever carries: npm_wrapper remembers an
# `npm ci` refused for any of these (see its memo).
sys.stdout.write('%s\n' % verdict['reason'])
return 3
sys.stderr.write('bun delivery declined: %s (%s)\n'
% (verdict.get('reason', '?'), verdict.get('detail', '')))
return 1
if __name__ == '__main__':
sys.exit(main())
utils/python_wrapper 0000755 00000001072 15272570413 0010721 0 ustar 00 #!/bin/bash
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
if [[ $EUID -eq 0 ]]; then
echo "This program is not intended to be run as root." 1>&2
exit 1
fi
CWD=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
source ${CWD}/activate
CL_PYTHON_VERSION="$(echo "${VIRTUAL_ENV}" | awk -F '/' '{print $NF}')"
eval "$(${CWD}/set_env_vars.py python)"
ABSOLUTE_PATH="${CWD}/python${CL_PYTHON_VERSION}_bin"
exec "${ABSOLUTE_PATH}" "$@"
utils/cloudlinux-cli-user.py 0000755 00000000753 15272570413 0012203 0 ustar 00 #!/opt/cloudlinux/venv/bin/python3 -sbb
# coding:utf-8
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
from cloudlinux_cli_user import CloudlinuxCliUser
if __name__ == "__main__":
cloudlinux_cli = CloudlinuxCliUser()
cloudlinux_cli.main()
utils/cloudlinux_bun_migration_cleanup.py 0000755 00000010706 15272570413 0015105 0 ustar 00 #!/opt/cloudlinux/venv/bin/python3 -bb
# coding:utf-8
"""Remove one account-owned Bun migration scratch transaction safely."""
import errno
import os
import re
import stat
import sys
TRANSACTION = re.compile(r'^\.cl-bun-migration-[0-9a-f]{32}$')
OPEN_DIRECTORY = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
def _remove_tree(directory_fd):
"""Delete entries by descriptor, refusing to traverse symlinked dirs.
One single post-order pass. Re-deriving each directory's path from the
transaction root cost one `open` per component, twice per directory --
quadratic in a depth the account chooses. Only the directory being
emptied holds a descriptor; a popped frame reaches its parent through
`..` and proves it is the inode it descended from.
"""
def open_directory(dir_fd, name):
child_fd = os.open(name, OPEN_DIRECTORY, dir_fd=dir_fd)
try:
return child_fd, os.fstat(child_fd)
except BaseException:
os.close(child_fd)
raise
current_fd = os.dup(directory_fd)
root_stat = os.fstat(current_fd)
frames = [{'name': None, 'names': None, 'index': 0,
'dev': root_stat.st_dev, 'ino': root_stat.st_ino}]
try:
while frames:
frame = frames[-1]
if frame['names'] is None:
frame['names'] = os.listdir(current_fd)
if frame['index'] >= len(frame['names']):
if frame['name'] is None:
frames.pop()
continue
parent_fd, parent_stat = open_directory(current_fd, '..')
above = frames[-2]
try:
if (parent_stat.st_dev != above['dev']
or parent_stat.st_ino != above['ino']):
raise OSError(errno.ENOTDIR,
'transaction changed while cleaning')
os.rmdir(frame['name'], dir_fd=parent_fd)
except BaseException:
os.close(parent_fd)
raise
os.close(current_fd)
current_fd = parent_fd
frames.pop()
continue
name = frame['names'][frame['index']]
frame['index'] += 1
entry_stat = os.stat(name, dir_fd=current_fd, follow_symlinks=False)
if stat.S_ISDIR(entry_stat.st_mode):
child_fd, child_stat = open_directory(current_fd, name)
os.close(current_fd)
current_fd = child_fd
frames.append({'name': name, 'names': None, 'index': 0,
'dev': child_stat.st_dev,
'ino': child_stat.st_ino})
else:
# A symlink is an entry in the transaction, never a path
# to traverse. unlinkat removes the link itself.
os.unlink(name, dir_fd=current_fd)
finally:
os.close(current_fd)
def clean(home, transaction):
if not TRANSACTION.match(transaction):
raise ValueError('invalid migration transaction')
home_fd = os.open(home, OPEN_DIRECTORY)
try:
try:
scratch_fd = os.open('.cl-bun-migration', OPEN_DIRECTORY, dir_fd=home_fd)
except OSError as exc:
if exc.errno == errno.ENOENT:
return
raise
try:
try:
transaction_fd = os.open(transaction, OPEN_DIRECTORY, dir_fd=scratch_fd)
except OSError as exc:
if exc.errno == errno.ENOENT:
return
raise
try:
_remove_tree(transaction_fd)
finally:
os.close(transaction_fd)
os.rmdir(transaction, dir_fd=scratch_fd)
finally:
os.close(scratch_fd)
try:
os.rmdir('.cl-bun-migration', dir_fd=home_fd)
except OSError as exc:
if exc.errno not in (errno.ENOENT, errno.ENOTEMPTY):
raise
finally:
os.close(home_fd)
def main(argv=None):
argv = sys.argv[1:] if argv is None else argv
if len(argv) != 2:
sys.stderr.write('usage: cloudlinux_bun_migration_cleanup HOME TRANSACTION\n')
return 2
try:
clean(argv[0], argv[1])
except (OSError, ValueError) as exc:
sys.stderr.write('migration scratch cleanup refused: %s\n' % exc)
return 1
return 0
if __name__ == '__main__':
sys.exit(main())
utils/bun_user_phase 0000755 00000120030 15272570413 0010636 0 ustar 00 #!/bin/bash
# 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
#
# User-side half of the bun install-time delivery, invoked through
# cagefs_enter_user by clselector/bun_delivery_hook.py. Normal delivery has
# three phases; explicit migration uses isolated resolve/rebuild variants:
#
# prepare canonicalize active lockfile inputs; conflicts refuse
# prepare-classic preserve divergent legacy prefix inputs for plain npm
# resolve bun install --lockfile-only -- the account's own registries
# and .npmrc; no package code runs
# rebuild npm rebuild -- lifecycle scripts and node-gyp,
# as the user, venv node on PATH
#
# The venv's activate must be sourced first: without it the venv node is
# not on PATH and .bin shims that exec a bare `node` die inside the cage.
set -u
phase="${1:-}"
app_root="${2:-}"
# Overridable so the phase can be driven against a stub in tests. No
# privilege implication: this half already runs as the account.
BUN="${CL_BUN_BIN:-/opt/cl-bun/bun}"
# Refuse before creating/changing any application inputs. Preparation,
# rebuild and cleanup do not use Bun and must remain available without it.
if [[ "$phase" == resolve || "$phase" == migration-resolve ]]; then
if [[ ! -f "$BUN" || ! -x "$BUN" ]]; then
echo 'Shared node_modules Store requires a usable cl-bun engine. Ask the administrator to run cl-node-modules-storage enable to restore it, or cl-node-modules-storage disable to use classic npm.' >&2
exit 1
fi
fi
CL_PYTHON=/opt/cloudlinux/venv/bin/python3
SHRINKWRAP_UNSUPPORTED='npm-shrinkwrap.json is not supported by Shared node_modules Store; use classic npm'
PACKAGE_LOCK_V1_UNSUPPORTED='package-lock.json lockfileVersion 1 is not supported by Shared node_modules Store; run classic npm install first'
NO_DEPENDENCIES='the application has no dependencies to share'
# Separates the fatal entries of .bun-buildable from the tolerated ones. A
# valid npm name no tree carries, so an older phase reading a newer file
# rebuilds the whole list fatally, exactly as it did before.
BUILDABLE_OPTIONAL_SECTION='cl-bun-optional-names'
MIGRATION_CLEANER="${MIGRATION_CLEANER:-/usr/share/l.v.e-manager/utils/cloudlinux_bun_migration_cleanup.py}"
copy_regular_nofollow() {
/opt/cloudlinux/venv/bin/python3 - "$1" "$2" "$3" <<'PY'
import os
import errno
import stat
import sys
source, target, label = sys.argv[1:]
source_fd = target_fd = None
created = False
try:
try:
source_fd = os.open(source, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW)
except OSError as error:
if error.errno == errno.ELOOP:
raise OSError('%s is not a regular file' % label)
raise
if not stat.S_ISREG(os.fstat(source_fd).st_mode):
raise OSError('%s is not a regular file' % label)
target_fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600)
created = True
while True:
chunk = os.read(source_fd, 1024 * 1024)
if not chunk:
break
offset = 0
while offset < len(chunk):
offset += os.write(target_fd, chunk[offset:])
except OSError as error:
sys.stderr.write('migration %s\n' % error)
if created:
try:
os.unlink(target)
except OSError:
pass
sys.exit(1)
finally:
if target_fd is not None:
os.close(target_fd)
if source_fd is not None:
os.close(source_fd)
PY
}
package_lock_state() {
"$CL_PYTHON" - "$1" <<'PY'
import errno
import json
import os
import stat
import sys
path = sys.argv[1]
maximum = 4 * 1024 * 1024
def no_duplicates(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError('duplicate JSON key')
result[key] = value
return result
fd = None
try:
try:
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW)
except OSError as error:
if error.errno == errno.ENOENT:
sys.exit(0)
raise
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_size > maximum:
raise ValueError('package-lock.json is not a bounded regular file')
chunks = []
remaining = maximum + 1
while remaining:
chunk = os.read(fd, min(1024 * 1024, remaining))
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
data = b''.join(chunks)
if len(data) > maximum:
raise ValueError('package-lock.json is too large')
lock = json.loads(data.decode('utf-8'), object_pairs_hook=no_duplicates)
if not isinstance(lock, dict):
raise ValueError('package-lock.json is not an object')
version = lock.get('lockfileVersion')
if type(version) is not int or version not in (1, 2, 3):
raise ValueError('unsupported package-lock.json lockfileVersion')
sys.exit(3 if version == 1 else 0)
except (OSError, UnicodeError, ValueError, RecursionError) as error:
sys.stderr.write('cannot inspect package-lock.json: %s\n' % error)
sys.exit(2)
finally:
if fd is not None:
os.close(fd)
PY
}
unsupported_manifest_field() {
# Print the first package.json field the Store cannot deliver, and exit 3.
# The root validator refuses the same fields, but bun resolves first, on a
# scratch directory that has none of the account's patch files: the account
# was told to "run bun patch" for a feature that would have been refused
# anyway. Read the manifest bounded, with duplicate keys rejected, the way
# package_lock_state reads the lock. A symlinked manifest is named as the
# token `symlink`: bun would follow it, but the privileged phase never
# does, so it is classic npm territory. A manifest that cannot even be
# opened is the root validator's business, not this pre-check's.
"$CL_PYTHON" - "$1" <<'PY'
import json
import os
import stat
import sys
path = sys.argv[1]
maximum = 4 * 1024 * 1024
unsupported = ('patchedDependencies', 'workspaces')
def no_duplicates(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError('duplicate JSON key')
result[key] = value
return result
fd = None
try:
# The privileged phase reads this same file through a component-wise
# O_NOFOLLOW walk (B4) and refuses a link; letting a linked manifest
# through here only moved that failure one phase later. Name it now.
try:
if stat.S_ISLNK(os.lstat(path).st_mode):
sys.stdout.write('symlink')
sys.exit(3)
except OSError:
pass
try:
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
except OSError:
# No opinion: nothing was read, so nothing is refused here.
sys.exit(0)
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_size > maximum:
raise ValueError('package.json is not a bounded regular file')
chunks = []
remaining = maximum + 1
while remaining:
chunk = os.read(fd, min(1024 * 1024, remaining))
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
data = b''.join(chunks)
if len(data) > maximum:
raise ValueError('package.json is too large')
manifest = json.loads(data.decode('utf-8'), object_pairs_hook=no_duplicates)
if not isinstance(manifest, dict):
raise ValueError('package.json is not an object')
for field in unsupported:
if field in manifest:
sys.stdout.write(field)
sys.exit(3)
sys.exit(0)
except (OSError, UnicodeError, ValueError, RecursionError) as error:
sys.stderr.write('cannot inspect package.json: %s\n' % error)
sys.exit(2)
finally:
if fd is not None:
os.close(fd)
PY
}
# The administrator migration must resolve without changing the active
# application symlink, venv package.json, lockfile, or cache. Its only
# account-side write is a transaction-named scratch directory below HOME.
# Handle it before the normal venv setup, which deliberately maintains those
# live links for ordinary delivery.
if [ "$phase" = "migration-resolve" ] || [ "$phase" = "migration-clean" ]; then
transaction="${4:-}"
[ -n "$app_root" ] || { echo "missing application root" >&2; exit 2; }
[[ "$transaction" =~ ^\.cl-bun-migration-[0-9a-f]{32}$ ]] || {
echo "invalid migration transaction" >&2; exit 2; }
scratch_root="$HOME/.cl-bun-migration"
scratch="$scratch_root/$transaction"
if [ "$phase" = "migration-clean" ]; then
exec "$MIGRATION_CLEANER" "$HOME" "$transaction"
exit $?
fi
umask 077
mkdir -p -- "$scratch_root" || exit 1
[ ! -e "$scratch" ] || { echo "migration scratch exists" >&2; exit 1; }
mkdir -- "$scratch" || exit 1
copy_regular_nofollow "$HOME/$app_root/package.json" \
"$scratch/package.json" package.json || exit 1
# Bun resolves the existing npm/yarn pins when they are present. Keep a
# root-side migration semantically equivalent to the application's next
# install without modifying any live application input. Symlinks are not
# copied: migration must not follow a caller-controlled detour out of the
# application directory.
for lock_input in package-lock.json npm-shrinkwrap.json yarn.lock; do
app_lock="$HOME/$app_root/$lock_input"
venv_lock="$HOME/nodevenv/$app_root/$3/lib/$lock_input"
app_present=0
venv_present=0
if [ -e "$app_lock" ] || [ -L "$app_lock" ]; then
[ -f "$app_lock" ] && [ ! -L "$app_lock" ] || {
echo "migration lock input is not a regular file: $lock_input" >&2; exit 1; }
app_present=1
fi
if [ -L "$venv_lock" ]; then
# Normal prepare makes this exact absolute alias, including a dangling
# one when npm has not yet written a lock. It is only another name for
# the app-root input, never a second migration source.
[ "$(readlink "$venv_lock")" = "$app_lock" ] || {
echo "migration venv lock input is not a regular file: $lock_input" >&2; exit 1; }
elif [ -e "$venv_lock" ]; then
[ -f "$venv_lock" ] || {
echo "migration venv lock input is not a regular file: $lock_input" >&2; exit 1; }
venv_present=1
fi
if [ "$app_present" -eq 1 ] && [ "$venv_present" -eq 1 ]; then
cmp -s -- "$app_lock" "$venv_lock" || {
conflict="migration lock input conflicts with active venv: $lock_input"
printf '%s\n' "$conflict" > "$scratch/diagnostic"
echo "$conflict" >&2
exit 1
}
fi
if [ "$lock_input" = npm-shrinkwrap.json ] \
&& { [ "$app_present" -eq 1 ] || [ "$venv_present" -eq 1 ]; }; then
printf '%s\n' "$SHRINKWRAP_UNSUPPORTED" > "$scratch/diagnostic"
echo "$SHRINKWRAP_UNSUPPORTED" >&2
exit 1
fi
if [ "$app_present" -eq 1 ]; then
copy_regular_nofollow "$app_lock" "$scratch/$lock_input" \
"$lock_input" || exit 1
elif [ "$venv_present" -eq 1 ]; then
copy_regular_nofollow "$venv_lock" "$scratch/$lock_input" \
"$lock_input" || exit 1
fi
done
package_lock_state "$scratch/package-lock.json"
package_lock_status=$?
if [ "$package_lock_status" -eq 3 ]; then
printf '%s\n' "$PACKAGE_LOCK_V1_UNSUPPORTED" > "$scratch/diagnostic"
echo "$PACKAGE_LOCK_V1_UNSUPPORTED" >&2
exit 3
elif [ "$package_lock_status" -ne 0 ]; then
exit 1
fi
cd "$scratch" || exit 1
"$BUN" install --lockfile-only --no-progress \
--cache-dir="$scratch/.bun-cache" 2>&1 | tail -c 2000 > "$scratch/diagnostic"
resolve_status="${PIPESTATUS[0]}"
# Zero packages means bun wrote no lockfile, so there is nothing for the
# privileged phase to validate. Migration is an explicit conversion, not
# an install, so it takes its own typed refusal here rather than the
# classic-npm route the install path takes.
if [ "$resolve_status" -eq 0 ] && [ ! -f bun.lock ]; then
printf '%s\n' "$NO_DEPENDENCIES" > "$scratch/diagnostic"
echo "$NO_DEPENDENCIES" >&2
exit 3
fi
exit "$resolve_status"
fi
dedupe_list() {
# Reads `dedupe_input`, writes `dedupe_output`, order preserving.
#
# `printf … | sort -u` would have to come back through a process
# substitution, and the cage has no /dev/fd: there the redirect fails,
# `mapfile` never runs, and the array keeps what it already held. Both
# lists here are populated before this point, so the effect was a
# redundant per-instance loop rather than a missing one -- but the
# construct cannot work in a cage at all, which is why it is banned
# outright rather than worked around.
# A here-string would only trade that for a writable TMPDIR. Bash alone
# needs neither. Order is first-occurrence, not `sort -u`'s: root sorts
# the file by path and this runs after stripping to install names, which
# reorders. Nothing depends on it -- one `npm rebuild` takes the required
# names as arguments, and the optional ones drive an independent loop.
#
# Quadratic in the number of buildable packages, which the tree walk
# already bounds and which is a handful in practice.
local candidate seen duplicate
dedupe_output=()
for candidate in ${dedupe_input[@]+"${dedupe_input[@]}"}; do
duplicate=0
for seen in ${dedupe_output[@]+"${dedupe_output[@]}"}; do
[ "$seen" = "$candidate" ] || continue
duplicate=1
break
done
[ "$duplicate" -eq 0 ] && dedupe_output+=("$candidate")
done
# Without this the function returns 1 whenever the *last* element was a
# duplicate, which no caller checks today and one would eventually.
return 0
}
drop_optional_package() {
# npm parity: an optional dependency whose install hook fails is removed
# and the install carries on. The package is a private copy -- the root
# phase detaches every buildable package before hand-over -- so this
# removes no inode the shared store still owns.
#
# Returns non-zero if the directory survives: leaving an unbuilt native
# package behind while reporting success would commit a tree the account
# cannot use.
local path="$1" install_name parent container target shim walked component \
rest ancestors
# `.bin` belongs to the `node_modules` container, never to a scope
# directory inside it, so the container is what is left once the whole
# install name -- scope included -- is taken off the end.
install_name="${path##*/node_modules/}"
parent="${path%"$install_name"}"
parent="${parent%/}"
container="$lib/node_modules"
[ -n "$parent" ] && container="$lib/node_modules/$parent"
# `rm -rf` resolves intermediate components, so a symlinked ancestor
# would delete outside the tree. Root's walk is O_NOFOLLOW at every
# component and can never emit such a path -- this is defence in depth,
# and what the "re-check rather than trust the file" claim beside the
# caller needs to be true. The leaf is not checked: `rm` unlinks a
# symlink there and leaves its target alone.
# Every component except the final one, so a scoped leaf's `@scope`
# directory is walked too: it sits between the container and the leaf,
# and `$parent` -- the path minus the *whole* install name -- skips it.
# The leaf itself is deliberately excluded: `rm` unlinks a symlink there
# and leaves its target, which is what npm would do.
#
# Parameter expansion rather than word splitting on IFS: an unquoted
# `$ancestors` would also be glob-expanded, and a package may
# legitimately be called `a*b`.
ancestors="${path%/*}"
[ "$ancestors" = "$path" ] && ancestors=""
walked="$lib/node_modules"
# The walk's own starting point counts: a symlinked `node_modules` takes
# every removal outside the venv however clean the components below it.
if [ -L "$walked" ]; then
echo "refusing to remove optional dependency $path through a" \
"symbolic link at node_modules" >&2
return 1
fi
rest="$ancestors"
while [ -n "$rest" ]; do
component="${rest%%/*}"
if [ "$component" = "$rest" ]; then rest=""; else rest="${rest#*/}"; fi
[ -n "$component" ] || continue
walked="$walked/$component"
if [ -L "$walked" ]; then
echo "refusing to remove optional dependency $path through the" \
"symbolic link $component" >&2
return 1
fi
done
rm -rf -- "$lib/node_modules/$path"
# `-e` follows the link, so a dangling symlink left at that path would
# read as removed while node still resolves it. `-L` catches that.
if [ -e "$lib/node_modules/$path" ] || [ -L "$lib/node_modules/$path" ]; then
echo "cannot remove optional dependency $path after its build" \
"failed" >&2
return 1
fi
# Only this package's own shims go. Another dangling link in the same
# .bin is the account's business: we were asked to drop one package.
if [ -d "$container/.bin" ]; then
for shim in "$container/.bin"/*; do
[ -L "$shim" ] || continue
target="$(readlink -- "$shim")" || continue
# `../lmdb/../other/cli.js` opens with the package's own prefix and
# then leaves it, so anything that walks back out is not ours.
case "$target" in
*"/../"*|*/..) continue ;;
esac
case "$target" in
"../$install_name"|"../$install_name"/*) rm -f -- "$shim" ;;
esac
done
fi
return 0
}
migration_rebuild=0
migration_scratch=""
classic_prepare=0
classic_conflict=0
if [ "$phase" = "migration-rebuild" ]; then
transaction="${4:-}"
[[ "$transaction" =~ ^\.cl-bun-migration-[0-9a-f]{32}$ ]] || {
echo "invalid migration transaction" >&2; exit 2; }
migration_scratch="$HOME/.cl-bun-migration/$transaction"
[ -d "$migration_scratch" ] || { echo "migration scratch missing" >&2; exit 1; }
migration_rebuild=1
phase=rebuild
elif [ "$phase" = "prepare-classic" ]; then
# Existing applications can legitimately have a Git-managed lock in the
# application root and a different lock written by the historical
# prefix-based npm path. Plain npm must remain available after upgrade,
# while Shared Store delivery continues to reject that ambiguity.
classic_prepare=1
phase=prepare
fi
# npm invokes --script-shell as `<shell> -c <command>`. Re-entering this
# script only for that form lets npm suppress automatic companion hooks while
# the application command itself sees its normal npm configuration.
if [ "$phase" = "-c" ]; then
unset npm_config_ignore_scripts
exec "${CL_BUN_NPM_SCRIPT_SHELL:-/bin/sh}" "$@"
fi
major="${3:-}"
[ -n "$phase" ] && [ -n "$app_root" ] || {
echo "usage: $0 <prepare|prepare-classic|resolve|deliver|rebuild> <app-root> [node-major]" >&2; exit 2; }
DELIVERY_CLIENT=/usr/share/l.v.e-manager/utils/cloudlinux_bun_client.py
# The delivery request needs neither a venv nor bun -- it is one sentence to
# a socket -- so it is answered before the interpreter discovery below,
# which would otherwise fail on an application the account has not built.
if [ "$phase" = "deliver" ]; then
[ -x "$CL_PYTHON" ] || { echo "python runtime unavailable" >&2; exit 2; }
exec "$CL_PYTHON" "$DELIVERY_CLIENT" "$app_root"
fi
# Which venv this install belongs to. A Node.js version change leaves the
# old `<venv_base>/<old>` beside the new one, so a listing of the directory
# has two answers and the lexically first is not the current one. The
# answer comes from whoever already knows: npm_wrapper's environment
# (`CL_VIRTUAL_ENV`, set by set_env_vars for exactly this application) or
# the panel hook's argument (the selector's recorded major). Only without
# either does the listing decide, and only when it has one entry.
venv_base="$HOME/nodevenv/$app_root"
if [ -n "${CL_VIRTUAL_ENV:-}" ] && [ -d "$CL_VIRTUAL_ENV" ]; then
venv="$CL_VIRTUAL_ENV"
elif [ -n "$major" ]; then
venv="$venv_base/$major"
else
# A glob loop rather than mapfile over a process substitution: the cage
# has no /dev/fd, and an empty array is fatal under `set -u` on bash 4.2.
found=""
count=0
for candidate in "$venv_base"/[0-9]*; do
[ -d "$candidate" ] || continue
found="$found ${candidate##*/}"
count=$((count + 1))
venv="$candidate"
done
if [ "$count" -ne 1 ]; then
echo "cannot tell which venv of $app_root to use: found${found:- none}" >&2
exit 1
fi
fi
[ -d "$venv" ] || { echo "no venv for $app_root at $venv" >&2; exit 1; }
lib="$venv/lib"
prepare_app_lockfile() {
local name="$1" mode="$2" app_lock lib_lock app_kind lib_kind
app_lock="$HOME/$app_root/$name"
lib_lock="$lib/$name"
if [[ -L "$app_lock" && "$classic_prepare" -eq 1 ]]; then
# A linked lockfile is the account's own layout and plain npm reads it
# as it always has. Keep the whole historical layout, like the
# divergent-copies case below: refusing here left such an application
# unable to `npm install` at all, even after `disable`.
classic_conflict=1
return 0
elif [[ -L "$app_lock" || ( -e "$app_lock" && ! -f "$app_lock" ) ]]; then
echo "refusing unexpected app lockfile: $app_lock" >&2
return 1
elif [[ -f "$app_lock" ]]; then
app_kind=regular
else
app_kind=absent
fi
if [[ -L "$lib_lock" ]]; then
if [[ "$(readlink "$lib_lock")" == "$app_lock" ]]; then
lib_kind=linked
else
echo "refusing unexpected venv lockfile link: $lib_lock" >&2
return 1
fi
elif [[ -e "$lib_lock" && ! -f "$lib_lock" ]]; then
echo "refusing unexpected venv lockfile: $lib_lock" >&2
return 1
elif [[ -f "$lib_lock" ]]; then
lib_kind=regular
else
lib_kind=absent
fi
case "$app_kind:$lib_kind" in
absent:regular)
[ "$mode" = preflight ] && return 0
mv -- "$lib_lock" "$app_lock" || return 1
;;
regular:regular)
if ! cmp -s -- "$app_lock" "$lib_lock"; then
if [ "$classic_prepare" -eq 1 ]; then
classic_conflict=1
return 0
fi
echo "conflicting lock files: $app_lock and $lib_lock" >&2
return 1
fi
[ "$mode" = preflight ] && return 0
rm -f -- "$lib_lock" || return 1
;;
regular:absent)
[ "$mode" = preflight ] && return 0
;;
absent:absent)
return 0
;;
regular:linked)
return 0
;;
absent:linked)
[ "$mode" = preflight ] && return 0
# Older wrappers created this exact dangling alias. npm 10 refuses
# to open it, so let npm materialize a regular prefix lock and
# canonicalize that file after the command succeeds.
rm -f -- "$lib_lock" || return 1
return 0
;;
*)
echo "refusing unexpected lockfile state: $app_lock $lib_lock" >&2
return 1
;;
esac
ln -s -- "$app_lock" "$lib_lock"
}
prepare_app_lockfiles() {
local lockfile
for lockfile in package-lock.json npm-shrinkwrap.json yarn.lock; do
prepare_app_lockfile "$lockfile" preflight || return 1
done
# Preserve the complete historical lockfile layout when any pair is
# ambiguous. This keeps classic npm compatible without partly
# canonicalizing unrelated inputs before the command runs.
[ "$classic_conflict" -eq 1 ] && return 0
for lockfile in package-lock.json npm-shrinkwrap.json yarn.lock; do
prepare_app_lockfile "$lockfile" apply || return 1
done
}
# The selector's activate script references unset variables, which is
# fatal under `set -u` -- relax it for the duration of the source only.
set +u
# shellcheck disable=SC1090
source "$venv/bin/activate" 2>/dev/null || true
set -u
# Both package managers work with the venv as their directory while the
# manifest lives in the application root, so the venv has to point at it.
# npm_wrapper does this before every npm run, and a freshly created
# application has only bin/ -- no lib/, no link. Measured without this on a
# new account: `cd <venv>/lib` fails, resolve exits 1, the hook falls back
# to plain npm, and that npm run creates both -- so delivery never covered
# an account's *first* install, the one install the shared cache is most
# likely to serve in full. Idempotent, and done here rather than root-side
# on purpose: this phase already runs as the account, so linking inside its
# own home stays an ordinary unprivileged operation.
if [ "$migration_rebuild" -eq 0 ]; then
# Classic npm mutations call `prepare` under their app lock. It needs a
# place for lockfile aliases, but must not touch the application's active
# node_modules tree (update/uninstall/rebuild operate on that tree).
mkdir -p "$lib" || { echo "cannot create $lib" >&2; exit 1; }
if [ "$phase" = resolve ]; then
# Engine incapability like a linked package.json, and the same route:
# delivery canonicalises these names and must not follow or replace the
# account's link, so name it before preparation refuses it outright.
for linked_lock in package-lock.json npm-shrinkwrap.json yarn.lock; do
if [ -L "$HOME/$app_root/$linked_lock" ]; then
printf '%s\n' 'cl-bun:unsupported-manifest symlink'
exit 3
fi
done
fi
prepare_app_lockfiles || exit 1
if [ "$phase" = prepare ]; then
exit 0
fi
mkdir -p "$lib/node_modules" || { echo "cannot create $lib/node_modules" >&2; exit 1; }
ln -sf "$HOME/$app_root/package.json" "$lib/package.json" || {
echo "cannot link package.json into $lib" >&2; exit 1; }
# Node resolves modules from <app_root>/node_modules, while the tree we
# deliver lives in the venv. npm_wrapper links the two on every install --
# and the panel path, the only entry point this feature supports, never
# reaches the wrapper. Without this a delivered application cannot load a
# single dependency: measured on an application installed only through the
# delivery, `require('express')` fails with MODULE_NOT_FOUND while the tree
# sits complete in the venv.
app_node_modules="$HOME/$app_root/node_modules"
if [ -L "$app_node_modules" ]; then
[ "$(readlink "$app_node_modules")" = "$lib/node_modules" ] || {
echo "$app_node_modules exists and is not the selected venv link" >&2; exit 1; }
elif [ -e "$app_node_modules" ]; then
# Same rule npm_wrapper enforces: a real directory there is the account's
# own tree and must not be silently replaced. Refusing drops the install
# onto the ordinary npm path, which reports the same condition.
echo "$app_node_modules exists and is not a symlink" >&2; exit 1
fi
if [ ! -e "$app_node_modules" ] && [ ! -L "$app_node_modules" ]; then
ln -s "$lib/node_modules" "$app_node_modules" || {
echo "cannot link node_modules into $HOME/$app_root" >&2; exit 1; }
fi
fi
cd "$lib" || exit 1
migration_command() {
if [ "$migration_rebuild" -eq 1 ]; then
"$@" 2>&1 | tail -c 2000 > "$migration_scratch/diagnostic"
return "${PIPESTATUS[0]}"
fi
"$@" >/dev/null 2>&1
}
rebuild_env_state="$lib/.cl-bun-rebuild-env"
is_rebuild_env_name() {
case "$1" in
npm_config_build_from_source|npm_config_arch|npm_config_platform|\
npm_config_target|npm_config_runtime|npm_config_nodedir|npm_config_devdir|\
npm_config_jobs|npm_config_python|npm_config_node_gyp|\
NPM_CONFIG_BUILD_FROM_SOURCE|NPM_CONFIG_ARCH|NPM_CONFIG_PLATFORM|\
NPM_CONFIG_TARGET|NPM_CONFIG_RUNTIME|NPM_CONFIG_NODEDIR|NPM_CONFIG_DEVDIR|\
NPM_CONFIG_JOBS|NPM_CONFIG_PYTHON|NPM_CONFIG_NODE_GYP|\
CC|CXX|CPP|AR|LD|MAKE|PYTHON|NODE_GYP_FORCE_PYTHON)
return 0
;;
esac
return 1
}
save_rebuild_environment() {
local name temporary
temporary="$rebuild_env_state.$$"
(umask 077; set -o noclobber; : > "$temporary") || return 1
for name in $(compgen -e); do
if is_rebuild_env_name "$name"; then
printf '%s=%s\0' "$name" "${!name}" >> "$temporary" || {
rm -f -- "$temporary"
return 1
}
fi
done
if [ -e "$rebuild_env_state" ] || [ -L "$rebuild_env_state" ]; then
[ -f "$rebuild_env_state" ] && [ ! -L "$rebuild_env_state" ] || {
rm -f -- "$temporary"
return 1
}
fi
mv -f -- "$temporary" "$rebuild_env_state"
}
restore_rebuild_environment() {
local entry name
[ -f "$rebuild_env_state" ] && [ ! -L "$rebuild_env_state" ] || return 0
trap 'rm -f -- "$rebuild_env_state"' EXIT
while IFS= read -r -d '' entry; do
[[ "$entry" == *=* ]] || continue
name="${entry%%=*}"
is_rebuild_env_name "$name" || continue
export "$entry"
done < "$rebuild_env_state"
}
case "$phase" in
resolve)
if [ -f "$HOME/$app_root/npm-shrinkwrap.json" ] \
&& [ ! -L "$HOME/$app_root/npm-shrinkwrap.json" ]; then
# Engine incapability like a v1 lockfile or an unsupported manifest
# field, and the same route: exit 3 with its own sentinel and classic
# npm installs. A published CLI pins its tree this way, and refusing
# left such an application unable to install at all -- while being
# told to use the npm that never ran. Migration keeps its own typed
# refusal, which is an explicit conversion, not an install.
printf '%s\n' 'cl-bun:unsupported-npm-shrinkwrap'
exit 3
fi
package_lock_state "$HOME/$app_root/package-lock.json"
package_lock_status=$?
if [ "$package_lock_status" -eq 3 ]; then
printf '%s\n' 'cl-bun:legacy-package-lock-v1'
exit 3
elif [ "$package_lock_status" -ne 0 ]; then
exit 1
fi
# Name the refusal here rather than letting bun fail first on a manifest
# feature the root validator would refuse anyway. A manifest that could
# not be read names no field and stays an ordinary failure.
unsupported_field="$(unsupported_manifest_field "$HOME/$app_root/package.json")"
unsupported_status=$?
if [ "$unsupported_status" -eq 3 ]; then
# Engine incapability, not a delivery failure: npm workspaces is
# mainstream and a monorepo root's bare `npm install` is exactly what
# the wrapper routes here. Take the `lockfileVersion 1` route -- exit
# 3, named by its own sentinel, and classic npm installs.
printf '%s %s\n' 'cl-bun:unsupported-manifest' "$unsupported_field"
exit 3
elif [ "$unsupported_status" -ne 0 ]; then
exit 1
fi
# `npm ci` (npm_wrapper's CL_BUN_COMMAND) installs package-lock.json and
# nothing else. bun prefers a bun.lock it finds over package-lock.json
# and silently re-resolves a package.json that drifted from it unless
# frozen (measured, bun 1.4.0), so resolve from package-lock.json alone
# and refuse drift; without one there is nothing exact to resolve. Any
# failure here leaves the verdict to classic `npm ci`, and root still
# compares the result with package-lock.json before delivering it.
frozen=()
if [ "${CL_BUN_COMMAND:-}" = ci ]; then
[ -f "$HOME/$app_root/package-lock.json" ] || exit 1
rm -f -- bun.lock || exit 1
frozen=(--frozen-lockfile)
fi
# The privileged delivery starts rebuild in a fresh account cage while
# retaining the old tree for rollback. Preserve only non-secret native
# build controls from the originating npm environment across that phase
# boundary; registry credentials and arbitrary account variables never
# enter this file.
save_rebuild_environment || exit 1
# Scratch state stays inside the venv, not in the account's dotfiles:
# the shared cache is deliberately unreachable from the cage.
#
# Not `exec`: bun's own diagnosis (a registry error, a typo, an auth
# failure) is the only thing that tells the account why resolution
# failed, so the output is captured -- bounded by `tail` -- and echoed
# on failure. This half already runs as the account, so echoing its own
# bun output leaks no privileged state. stdout stays clean because the
# wrapper and the hook parse it for the phase sentinels above.
resolve_output="$(set -o pipefail
"$BUN" install --lockfile-only \
${frozen[@]+"${frozen[@]}"} --no-progress \
--cache-dir="$lib/.bun-cache" 2>&1 | tail -c 2000)"
resolve_status=$?
if [ "$resolve_status" -ne 0 ]; then
# Same reasoning as the no-dependencies route below: no exit that
# leaves this install to classic npm may leave native build settings
# in the venv for a rebuild that never comes.
rm -f -- "$rebuild_env_state"
# A failed `ci` resolve goes to classic `npm ci`, whose own error is
# the one the account should read.
if [ "${CL_BUN_COMMAND:-}" != ci ]; then
echo 'Shared node_modules Store: dependency resolution failed:' >&2
[ -n "$resolve_output" ] && printf '%s\n' "$resolve_output" >&2
fi
exit "$resolve_status"
fi
# Bun writes no lockfile at all when the manifest resolves to zero
# packages, and deletes one it wrote earlier ("No packages! Deleted
# empty lockfile"). There is then nothing for the privileged phase to
# read, and it used to fail the install on the missing file -- which is
# what the panel's "Run NPM Install" does on a freshly created
# application. Same route as the other inputs this engine cannot serve:
# exit 3 with its own sentinel, and classic npm performs the no-op
# install. A tree delivered before losing its last dependency is
# detached by the wrapper's classic path, as on every other exit 3.
if [ ! -f bun.lock ]; then
# The other three sentinels exit before `save_rebuild_environment`
# and so leave nothing behind. This one cannot: an empty resolution
# is what reveals the case, and that is only knowable once bun has
# run. So it clears the file it already wrote, rather than leaving
# native-build settings in the venv for a rebuild this route never
# reaches -- classic npm installs from here.
rm -f -- "$rebuild_env_state"
printf '%s\n' 'cl-bun:no-dependencies'
exit 3
fi
exit 0
;;
rebuild)
# This phase already runs inside npm_wrapper's app lock. Use the real
# alt-node npm directly so a manual `npm rebuild` can be protected by the
# wrapper without the internal lifecycle work trying to lock/unshare its
# own freshly delivered tree again.
NPM="${CL_NODEHOME:-}/usr/bin/npm"
[ -x "$NPM" ] || { echo "node npm unavailable at $NPM" >&2; exit 1; }
if [ "$migration_rebuild" -eq 0 ]; then
restore_rebuild_environment
fi
# npm parity: the npm path runs dependency lifecycle scripts
# unconditionally, and here they run as the user. The root phase
# leaves the short list of packages that actually carry an install
# hook; rebuilding the whole tree instead is the dominant cost of the
# flow. An absent list means "unknown", so rebuild everything.
if [ -f "$lib/.bun-buildable" ]; then
mapfile -t entries < "$lib/.bun-buildable"
# Everything before the section header must rebuild; everything after
# it is an optional dependency, which npm itself drops rather than
# failing the install when its hook fails. A file written before the
# section existed has no header, so every entry stays required.
pkgs=()
optional_paths=()
in_optional=0
# `${a[@]+"${a[@]}"}`: bash 4.2 (CL7) treats "${a[@]}" of an empty
# array as unbound, and this script runs under `set -u`. `entries` is
# empty for an empty manifest and `optional_paths` whenever there is
# no optional section, which is the ordinary case.
for entry in ${entries[@]+"${entries[@]}"}; do
if [ "$in_optional" -eq 0 ] && [ "$entry" = "$BUILDABLE_OPTIONAL_SECTION" ]; then
in_optional=1
continue
fi
if [ "$in_optional" -eq 1 ]; then
# Root produced these paths from its own walk, but this phase
# deletes by them, so re-check them here rather than trust the
# file: relative, and no parent traversal.
case "$entry" in
''|/*|*/../*|../*|*/..) echo "unsafe buildable entry: $entry" >&2; exit 1 ;;
..) echo "unsafe buildable entry: $entry" >&2; exit 1 ;;
esac
optional_paths+=("$entry")
else
pkgs+=("$entry")
fi
done
if [ "${#pkgs[@]}" -ne 0 ]; then
# The root phase records paths for private copies. npm 6 does not
# rebuild directory arguments; names select every installed version.
# Keep @scope/name intact. All buildable instances are private now.
for i in "${!pkgs[@]}"; do
pkgs[i]="${pkgs[i]##*/node_modules/}"
done
dedupe_input=(${pkgs[@]+"${pkgs[@]}"})
dedupe_list
pkgs=(${dedupe_output[@]+"${dedupe_output[@]}"})
migration_command "$NPM" rebuild -- "${pkgs[@]}" || exit $?
fi
# Required first, then the tolerated names. npm builds one dependency
# graph in its own order; this cannot, because a required failure has
# to stop the transaction before anything optional is removed. A
# package that needs an optional sibling built first would therefore
# differ from npm -- none is known, and npm does not promise that
# order either.
# Grouped by install name, and one `npm rebuild` per name: a name
# selector is name-wide -- it rebuilds every instance carrying it --
# so a failure cannot be attributed to one instance. npm 8 and 10 do
# accept a `./path` spec that would select just one, but npm 6 does
# not, and this has to behave the same on every major we deliver to.
# Root only tolerates a name when *every* buildable instance of it is
# optional (`_required_buildable`), so dropping all of them removes
# nothing npm would have kept, and never leaves a broken instance
# behind. One rebuild per name also keeps a single failure from
# hiding every later optional package.
optional_names=()
for optional_path in ${optional_paths[@]+"${optional_paths[@]}"}; do
optional_names+=("${optional_path##*/node_modules/}")
done
dedupe_input=(${optional_names[@]+"${optional_names[@]}"})
dedupe_list
optional_names=(${dedupe_output[@]+"${dedupe_output[@]}"})
for optional_name in ${optional_names[@]+"${optional_names[@]}"}; do
migration_command "$NPM" rebuild -- "$optional_name" && continue
for optional_path in ${optional_paths[@]+"${optional_paths[@]}"}; do
[ "${optional_path##*/node_modules/}" = "$optional_name" ] || continue
drop_optional_package "$optional_path" || exit 1
done
echo "optional dependency $optional_name failed to build and was" \
"removed; the application runs without it" >&2
done
else
migration_command "$NPM" rebuild || exit $?
fi
# `npm rebuild` replays dependency hooks but not this application's
# install lifecycle. Only invoke declared lifecycle events so a pure-JS
# application keeps the no-npm shortcut. JSON parsing is done by the
# venv node already required for npm; the emitted names come from this
# fixed list, not from the manifest.
lifecycle_events=$(node -e '
const fs = require("fs");
const events = ["preinstall", "install", "postinstall", "prepublish",
"preprepare", "prepare", "postprepare"];
const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
const scripts = manifest.scripts;
if (scripts && typeof scripts === "object" && !Array.isArray(scripts)) {
for (const event of events) {
if (typeof scripts[event] === "string") console.log(event);
}
}
' "$lib/package.json") || exit $?
[ -n "$lifecycle_events" ] || exit 0
# `--ignore-scripts` makes npm run only the explicitly requested event,
# rather than also running pre<event>/post<event> companions. npm passes
# that config into the script environment, though, so re-enter this
# script through its documented --script-shell hook to clear it before
# the application command starts. Preserve a user-configured script-shell
# (or npm's /bin/sh default) for the command itself.
lifecycle_shell=$("$NPM" config get script-shell 2>/dev/null) || exit $?
case "$lifecycle_shell" in
''|null|undefined) lifecycle_shell=/bin/sh ;;
esac
for lifecycle_script in $lifecycle_events; do
if [ "$migration_rebuild" -eq 1 ]; then
migration_command env CL_BUN_NPM_SCRIPT_SHELL="$lifecycle_shell" \
"$NPM" run --ignore-scripts --script-shell "$0" \
"$lifecycle_script" --if-present || exit $?
else
CL_BUN_NPM_SCRIPT_SHELL="$lifecycle_shell" \
"$NPM" run --ignore-scripts --script-shell "$0" \
"$lifecycle_script" --if-present >/dev/null 2>&1 || exit $?
fi
done
;;
*)
echo "unknown phase: $phase" >&2; exit 2
;;
esac
utils/cpanel_api.py 0000644 00000026614 15272570413 0010370 0 ustar 00 # coding:utf-8
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2020 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
from __future__ import absolute_import
from __future__ import print_function
from lvemanager.helpers import exit_with_error, run_command
import json
import re
from abc import abstractmethod
from clcommon.lib.whmapi_lib import WhmApiRequest, WhmApiError
import sys
OWNER_ADMIN = 'admin'
OWNER_RESELLER = 'reseller'
OWNER_USER = 'user'
UAPI_IGNORED_ERRORS = ["The event UAPI::LangPHP::php_set_vhost_versions was handled successfully."]
def get_cpanel_api_class(owner, username=None):
"""
Factory method that returns
certain class depending on
owner's type
:param owner: Can be 'admin', 'reseller' or 'user'
:param username: required when owner == 'reseller' so the UAPI call is
bound to the reseller's cPanel account
:return:
"""
if owner == OWNER_ADMIN:
return CPanelAdminApi()
if owner == OWNER_RESELLER:
# Reseller path runs as root (see drop_permission in
# cloudlinux_cli.py:157); UAPI inherits no implicit scope from
# cpsrvd env, so the username must be passed explicitly so the
# call is bound to the reseller's own cPanel account.
if not username:
exit_with_error('Reseller cpanel-api call requires username')
return CPanelUserApi(scoped_user=username)
return CPanelUserApi()
class CPanelApi(object):
"""
Abstract method that defines abstract methods
that must be implemented and common methods of
the derived classes
"""
RETURN_JSON = '--output=json'
ALLOWED_OPERATIONS = {}
@abstractmethod
def check_operation_allowed(self, called_method):
"""
Checks whether operation is allowed to perform
:param called_method:
:return:
"""
raise NotImplementedError('Method check_operation_allowed must be implemented!')
@abstractmethod
def prepare_running_command(self, called_method, params, return_json):
"""
Depending on the passed arguments,
the method builds running command
"""
raise NotImplementedError('Method prepare_running_command must be implemented!')
@abstractmethod
def parse_result(self, called_method, result):
"""
Some results are parsed before sending back
to the called. This method parses it and returns
transformed result.
"""
raise NotImplementedError('Method parse_result must be implemented!')
@staticmethod
def check_method_allowed(method_name, methods):
"""
CPanel API contains several methods and
not all of them if supported by this utility.
"""
for method_object in methods:
if method_name == method_object['method']:
return True
return False
def check_for_errors(self, result):
"""
Checks whether CPanel API returned
error or not
:param result:
:return:
"""
raise NotImplementedError('Method check_for_errors must be implemented!')
@staticmethod
def get_method_parser(method_name, methods):
"""
Some result must be parsed before
they are sent back to the caller. This
method extracts the method that must be executed
upon the result to transform it.
"""
for method_object in methods:
if method_name == method_object['method']:
return method_object.get('parser', None)
def get_ignore_errors(self, method_name, methods):
"""
return ignore error flag for method
:return:
"""
for method_object in methods:
if method_name == method_object['method']:
return method_object.get('ignore_errors', None)
@staticmethod
def parse_vhost_versions(result):
"""
Method parses the result of the method 'parse_vhost_versions'.
The host will be inherited when its field 'sys_default'
inside 'php_version_source' is equal to 1.
"""
parsed_result = []
for r in result:
parsed_result.append({
'version': r.get('version'),
'host': r.get('vhost'),
'php_fpm': True if r.get('php_fpm') else False,
'inherited': True if r.get('php_version_source', {}).get('sys_default') == 1 else False
})
return parsed_result
def run(self, called_method, params, return_json=True):
"""
Default method which first checks whether operation
is allowed then if allowed runs prepared command.
After receiving the result, it will call parse_result method
to transform result into desired format.
"""
if self.check_operation_allowed(called_method):
prepared_command = self.prepare_running_command(called_method, params, return_json)
code, output, std_err = run_command(prepared_command, return_full_output=True)
if code != 0:
exit_with_error(std_err or 'output of the command: %s\n%s' % (prepared_command, output))
try:
result = json.loads(output)
return self.parse_result(called_method, result)
except ValueError as e:
exit_with_error(e)
else:
exit_with_error('Not allowed operation: {}'.format(called_method))
class CPanelAdminApi(CPanelApi):
COMMAND = '/usr/local/cpanel/bin/whmapi1'
ALLOWED_OPERATIONS = [
{
'method': 'php_get_vhost_versions',
'parser': lambda result: CPanelAdminApi.parse_vhost_versions(result)
},
{'method': 'php_get_system_default_version'},
{'method': 'php_set_vhost_versions'},
{'method': 'php_get_installed_versions'}
]
def check_operation_allowed(self, called_method):
return self.check_method_allowed(called_method, self.ALLOWED_OPERATIONS)
def prepare_running_command(self, called_method, params, return_json):
transformed_params = {}
for param in params:
key, value = param.split('=')
transformed_params[key] = value
return transformed_params
def parse_result(self, called_method, result):
parser = self.get_method_parser(called_method, self.ALLOWED_OPERATIONS)
if parser is not None:
return parser(result)
return result
@staticmethod
def parse_vhost_versions(result):
parsed_result = []
for r in result['versions']:
parsed_result.append({
'version': r.get('version'),
'host': r.get('vhost'),
'user': r.get('account'),
'php_fpm': True if r.get('php_fpm') else False,
'inherited': True if r.get('php_version_source', {}).get('sys_default') == 1 else False
})
return parsed_result
def run(self, called_method, params, return_json=True):
params = self.prepare_running_command(called_method, params, return_json)
if self.check_operation_allowed(called_method):
try:
return self.parse_result(called_method, WhmApiRequest(called_method).with_arguments(**params).call())
except WhmApiError as e:
print(e)
sys.exit(1)
else:
exit_with_error('Not allowed operation: {}'.format(called_method))
class CPanelUserApi(CPanelApi):
COMMAND = '/usr/bin/uapi'
USER_BINDING_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]*$')
ALLOWED_OPERATIONS = {
'LangPHP': [
{'method': 'php_set_vhost_versions'},
{
'method': 'php_get_installed_versions',
'ignore_errors': True,
},
{
'method': 'php_get_vhost_versions',
'parser': lambda result: CPanelApi.parse_vhost_versions(result),
'ignore_errors': True,
},
{
'method': 'php_get_system_default_version',
'ignore_errors': True,
}
]
}
def __init__(self, scoped_user=None):
# scoped_user pins the UAPI call to a specific cPanel account by
# prepending `--user=<scoped_user>` to argv. Set for the reseller
# path (see get_cpanel_api_class) so a reseller cannot influence
# state on accounts other than their own. Left None for the
# `owner == 'user'` path where the process already drops uid to
# the user and UAPI infers scope from kernel identity.
if scoped_user is not None and not self.USER_BINDING_RE.match(scoped_user):
exit_with_error('Invalid scoped_user: {}'.format(scoped_user))
self.scoped_user = scoped_user
def check_class_allowed(self, class_name):
"""
CPanel UAPI requires to pass class name and
only few of classes are supported by this utility.
"""
return class_name in self.ALLOWED_OPERATIONS.keys()
def check_operation_allowed(self, called_method):
"""
Checks whether operation is allowed or not.
By CPanel UAPI supports several operations
but only few of them can be called via this utility.
"""
class_name, method_name = self.extract_class_and_method(called_method)
if self.check_class_allowed(class_name):
return self.check_method_allowed(method_name, self.ALLOWED_OPERATIONS[class_name])
return False
@staticmethod
def extract_class_and_method(called_method):
try:
class_name, method_name = called_method.split('::')
return class_name, method_name
except ValueError:
exit_with_error('Invalid method is passed: {}'.format(called_method))
def prepare_running_command(self, called_method, params, return_json):
class_name, method_name = self.extract_class_and_method(called_method)
argv = [self.COMMAND]
if self.scoped_user:
# Bind the call to a specific cPanel account; mirrors the
# `cpapi2 --user=$CURRENT_USER` shape used by
# cpanel/cgi/CloudLinux.pm:282-283 for the same reason.
argv.append('--user=' + self.scoped_user)
argv += [class_name, method_name] + params
if return_json:
argv.append(self.RETURN_JSON)
return argv
def parse_result(self, called_method, result):
class_name, method_name = self.extract_class_and_method(called_method)
ignore_errors = self.get_ignore_errors(method_name, self.ALLOWED_OPERATIONS[class_name])
self.check_for_errors(result, ignore_errors)
parser = self.get_method_parser(method_name, self.ALLOWED_OPERATIONS[class_name])
if parser is not None:
return parser(result['result']['data'])
return result['result']['data']
def check_for_errors(self, result, ignore_errors = False):
errors = result['result'].get('errors')
if not errors:
return
for error in errors:
# TODO: The next line is workaround of cPanel issue CPANEL-35122
# see https://support.cpanel.net/hc/en-us/articles/1500004317181-PHP-Selector-change-to-CloudLinux-PHP-version-reports-a-false-error
if error in UAPI_IGNORED_ERRORS:
continue
exit_with_error(' '.join(errors), ignore_errors=ignore_errors)
utils/node_wrapper 0000755 00000000416 15272570413 0010326 0 ustar 00 #!/bin/bash
if [[ $EUID -eq 0 ]]; then
echo "This program is not intended to be run as root." 1>&2
exit 1
fi
CWD=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
source ${CWD}/activate
eval "$(${CWD}/set_env_vars.py nodejs)"
exec "${CL_NODEHOME}/usr/bin/node" "$@"