Files
Linux_post_install/bin/pos-communication-scrcpy
Your Name d817c37652
gates / consistency-and-conventions (push) Successful in 26s
fix: stabilization pass — fail-closed auth, ai flag validation, lint/config/security hardening, regression tests
17-point code-level audit executed via Explorer->Architect->Builder->Tester->Reviewer;
Reviewer accepted (APPROVE_WITH_NOTES; 3 block-list items resolved):

- security: telegram sender-owner AND-gate + TELEGRAM_OWNER_ID, matrix
  MATRIX_ROOM_ID fail-closed, gpg --passphrase-fd 3 (no argv secret),
  /dev/tcp positional-arg form (checkport/smb-client/share-lib/NET_PROBE),
  eval deny-by-default + --no-command-execution carried by both chat bridges,
  tty-gated --trust; config/{telegram,matrix}.env reference templates
- ai: all ExecStart flags validated against installed llama.cpp
  (requested->error, default->omit+warn, CONFIG_REQUESTED_FLAGS); single-file
  hf download failure rc=1 + no .hf-meta; LLAMACPP_HOST coherent;
  POS_SUBCMDS + metadata gaps closed
- tooling: lint-conventions Bash-native rewrite (~24-30x faster, rules and
  output byte-identical, :num restored); pos system uninstall covers all 12
  libs + scale-tail + flags dir + systemd user units (|| true) + plugin
  markers; anchored .bash_completion/.bashrc removal replaces sed -i '/pos/d'
- config: canonical load_env_file in lib/config-ui.sh (CRLF strip, env-wins,
  XDG, LOADED_ENV_KEYS); 9 tools migrated; entertainment-lib collapsed to
  wrappers; docker-compose deliberately unmigrated (source semantics)
- tests: first committed regression suite — tests/run-tests.sh zero-dep
  runner + make test; 12 files / 179 checks / 0 skip / ~52s; hard skip
  contract; systemd-analyze verify on generated unit PASS

Verified: make gen idempotent; make check green; make lint 0 FAIL, 0 WARN;
make test green; bash -n clean; git diff --check clean. Audit deliverables +
agent reports + AGENT_TODO Done entry included.
2026-09-06 07:25:44 -04:00

246 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# POS: communication scrcpy — Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info)
# POS_SUBCMDS: devices record tcpip connect push pull screenshot info
# POS_CONFIG: scrcpy | scrcpy.env | SCRCPY_SERIAL=:Default adb serial (from 'pos communication scrcpy devices') | SCRCPY_MAX_SIZE=:Limit video size (e.g. 1920) | SCRCPY_MAX_FPS=:Limit frame rate (e.g. 60) | SCRCPY_BIT_RATE=:Video bit rate (e.g. 8M) | SCRCPY_FULLSCREEN=:Start fullscreen (true/false) | SCRCPY_NEW_DISPLAY=:New virtual display (--new-display): true, WxH, WxH/DPI or /DPI | SCRCPY_AUDIO=:Forward device audio to the desktop (true/false, default true) | SCRCPY_RECORD_DIR=:Record output dir (default ~/Videos/scrcpy) | SCRCPY_PUSH_TARGET=:adb push destination (default /sdcard/Download) | SCRCPY_EXTRA_FLAGS=:Extra scrcpy flags appended to every mirror
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
CONFIG_FILE="$CONFIG_DIR/scrcpy.env"
command -v scrcpy &>/dev/null || err "scrcpy not found — install the latest release with the app installer: 'apps/media/scrcpy.sh' (or 'sudo apt install scrcpy' if your distro ships it; it bundles adb), see 'pos help communication scrcpy'"
command -v adb &>/dev/null || err "adb not found — install it: 'sudo apt install adb'"
load_config() {
load_env_file "$CONFIG_FILE"
}
load_config
SCRCPY_SERIAL="${SCRCPY_SERIAL:-}"
SCRCPY_MAX_SIZE="${SCRCPY_MAX_SIZE:-}"
SCRCPY_MAX_FPS="${SCRCPY_MAX_FPS:-}"
SCRCPY_BIT_RATE="${SCRCPY_BIT_RATE:-}"
SCRCPY_FULLSCREEN="${SCRCPY_FULLSCREEN:-false}"
SCRCPY_RECORD_DIR="${SCRCPY_RECORD_DIR:-$HOME/Videos/scrcpy}"
SCRCPY_PUSH_TARGET="${SCRCPY_PUSH_TARGET:-/sdcard/Download}"
SCRCPY_EXTRA_FLAGS="${SCRCPY_EXTRA_FLAGS:-}"
SCRCPY_NEW_DISPLAY="${SCRCPY_NEW_DISPLAY:-}"
SCRCPY_AUDIO="${SCRCPY_AUDIO:-true}"
usage() {
cat <<EOF
Usage: pos communication scrcpy [command] [args]
Mirror and control an Android device with scrcpy (a GUI window; needs a
display — over ssh use 'ssh -X'). Over USB with USB debugging enabled, or
wirelessly via 'tcpip'/'connect'.
Commands:
(none) Mirror the device (config defaults + pass-through)
devices List connected devices (adb devices -l)
record [file] [--headless] Record a session to an mp4 (default:
\$SCRCPY_RECORD_DIR/<device>_<date>.mp4).
--headless adds --no-playback (no window)
tcpip [port] Switch the USB device to wireless adb (default 5555)
connect <ip[:port]> adb connect + mirror over WiFi
push <local> [remote] Copy a file/folder to the device (default
\$SCRCPY_PUSH_TARGET, i.e. /sdcard/Download)
pull <remote> [local] Copy a file/folder from the device (default: cwd)
screenshot [file] Save a PNG of the screen (default:
\$SCRCPY_RECORD_DIR/<date>.png)
info Show device model, Android version, SDK, serial
Pass-through: any scrcpy flag given to the bare form is forwarded verbatim
(e.g. 'pos communication scrcpy --turn-screen-off --stay-awake').
All scrcpy options: run 'scrcpy --help'.
Config: $CONFIG_FILE (SCRCPY_*) — edit it with 'pos config scrcpy'
Precedence: CLI flags > environment > config file.
Examples:
pos communication scrcpy
pos communication scrcpy --no-audio --always-on-top
pos communication scrcpy --new-display=1920x1080
pos communication scrcpy devices
pos communication scrcpy record --headless
pos communication scrcpy tcpip 5555
pos communication scrcpy connect 192.168.1.42:5555
pos communication scrcpy push ~/file.apk
pos communication scrcpy pull /sdcard/DCIM/Camera
pos communication scrcpy screenshot shot.png
pos communication scrcpy info
EOF
exit 0
}
case "${1:-}" in
-h|--help) usage ;;
esac
# ── helpers ────────────────────────────────────────────────────
_sflag() { [ -n "$SCRCPY_SERIAL" ] && printf -- '-s %s' "$SCRCPY_SERIAL"; }
# Print the serial of the first reachable device, or nothing.
_first_device() {
adb devices 2>/dev/null | awk 'NR>1 && $2=="device" {print $1; exit}'
}
# Exit 1 with a friendly hint when the target device isn't reachable.
_require_device() {
local serial="${1:-$SCRCPY_SERIAL}"
if [ -n "$serial" ]; then
if ! adb -s "$serial" get-state 2>/dev/null | grep -qx device; then
err "device '$serial' not reachable — is USB debugging enabled? (plug it in, or 'pos communication scrcpy connect <ip>')"
fi
elif [ -z "$(_first_device)" ]; then
err "no device connected — 'pos communication scrcpy devices' to list, or 'pos communication scrcpy connect <ip[:port]>' for wireless"
fi
}
_dev_name() { printf '%s' "${SCRCPY_SERIAL:-device}" | tr ':' '-'; }
_extra_flags() {
[ -n "$SCRCPY_EXTRA_FLAGS" ] || return 0
local -a extra
read -r -a extra <<< "$SCRCPY_EXTRA_FLAGS"
printf '%s\n' "${extra[@]}"
}
# Build the scrcpy command from config defaults + pass-through args.
_mirror() { # $@ = pass-through scrcpy flags
local -a cmd=(scrcpy)
local f
[ -n "$SCRCPY_SERIAL" ] && cmd+=(-s "$SCRCPY_SERIAL")
[ -n "$SCRCPY_MAX_SIZE" ] && cmd+=(--max-size "$SCRCPY_MAX_SIZE")
[ -n "$SCRCPY_MAX_FPS" ] && cmd+=(--max-fps "$SCRCPY_MAX_FPS")
[ -n "$SCRCPY_BIT_RATE" ] && cmd+=(--video-bit-rate "$SCRCPY_BIT_RATE")
[ "$SCRCPY_FULLSCREEN" = "true" ] && cmd+=(--fullscreen)
case "${SCRCPY_NEW_DISPLAY:-}" in
true|yes) cmd+=(--new-display) ;;
[0-9]*x[0-9]*|/[0-9]*) cmd+=(--new-display="$SCRCPY_NEW_DISPLAY") ;;
"") : ;;
*) err "invalid SCRCPY_NEW_DISPLAY '$SCRCPY_NEW_DISPLAY' — use true, <W>x<H>, <W>x<H>/<DPI> or /<DPI> (e.g. 1920x1080, 1920x1080/420, /240)" ;;
esac
case "${SCRCPY_AUDIO:-true}" in
true|yes|1) : ;;
false|no|0) cmd+=(--no-audio) ;;
*) err "invalid SCRCPY_AUDIO '$SCRCPY_AUDIO' — use true or false" ;;
esac
while IFS= read -r f; do [ -n "$f" ] && cmd+=("$f"); done < <(_extra_flags)
cmd+=("$@")
exec "${cmd[@]}"
}
# ── subcommands ────────────────────────────────────────────────
cmd_devices() {
adb devices -l
}
cmd_record() {
_require_device
local headless=0 file=""
while [ $# -gt 0 ]; do
case "$1" in
--headless) headless=1 ;;
-*) err "unknown record option: $1 (options: [file] [--headless])" ;;
*) [ -z "$file" ] && file="$1" || err "too many arguments: $1" ;;
esac
shift
done
mkdir -p "$SCRCPY_RECORD_DIR"
file="${file:-$SCRCPY_RECORD_DIR/$(_dev_name)_$(date +%Y%m%d_%H%M%S).mp4}"
log "recording to $file"
local -a cmd=(scrcpy $(_sflag) "--record=$file")
[ "$headless" -eq 1 ] && cmd+=(--no-playback)
exec "${cmd[@]}"
}
cmd_tcpip() {
_require_device
local port="${1:-5555}"
[[ "$port" =~ ^[0-9]+$ ]] || err "port must be a number (got '$port')"
adb $(_sflag) tcpip "$port"
local ip
ip="$(adb $(_sflag) shell 'ip route get 1.1.1.1 2>/dev/null' 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')"
if [ -n "$ip" ]; then
log "device is now listening on tcpip port $port — reconnect wirelessly with:"
log " pos communication scrcpy connect $ip:$port"
else
log "device is now listening on tcpip port $port — reconnect wirelessly with:"
log " pos communication scrcpy connect <device-ip>:$port"
warn "could not auto-detect the device IP — find it with 'adb $(_sflag) shell ip addr'"
fi
}
cmd_connect() {
local target="${1:-}"
[ -n "$target" ] || err "usage: pos communication scrcpy connect <ip[:port]>"
log "adb connect $target"
adb connect "$target"
SCRCPY_SERIAL="$target"
_require_device "$target"
_mirror
}
cmd_push() {
_require_device
local local_path="${1:-}" remote="${2:-$SCRCPY_PUSH_TARGET}"
[ -n "$local_path" ] || err "usage: pos communication scrcpy push <local> [remote]"
[ -e "$local_path" ] || err "no such file or folder: $local_path"
log "adb push $local_path$remote"
adb $(_sflag) push "$local_path" "$remote"
}
cmd_pull() {
_require_device
local remote="${1:-}" local_path="${2:-.}"
[ -n "$remote" ] || err "usage: pos communication scrcpy pull <remote> [local]"
log "adb pull $remote$local_path"
adb $(_sflag) pull "$remote" "$local_path"
}
cmd_screenshot() {
_require_device
local file="${1:-}"
if [ -z "$file" ]; then
mkdir -p "$SCRCPY_RECORD_DIR"
file="$SCRCPY_RECORD_DIR/$(_dev_name)_$(date +%Y%m%d_%H%M%S).png"
fi
log "screenshot → $file"
adb $(_sflag) exec-out screencap -p > "$file"
}
cmd_info() {
_require_device
local model android sdk serial
model="$(adb $(_sflag) shell getprop ro.product.model | tr -d '\r')"
android="$(adb $(_sflag) shell getprop ro.build.version.release | tr -d '\r')"
sdk="$(adb $(_sflag) shell getprop ro.build.version.sdk | tr -d '\r')"
serial="$(adb $(_sflag) shell getprop ro.serialno | tr -d '\r')"
printf '%-12s %s\n' "model:" "${model:-unknown}"
printf '%-12s %s\n' "android:" "${android:-unknown}"
printf '%-12s %s\n' "sdk:" "${sdk:-unknown}"
printf '%-12s %s\n' "serial:" "${serial:-unknown}"
}
# ── dispatch ───────────────────────────────────────────────────
cmd="${1:-}"
case "$cmd" in
devices) shift; cmd_devices "$@" ;;
record) shift; cmd_record "$@" ;;
tcpip) shift; cmd_tcpip "$@" ;;
connect) shift; cmd_connect "$@" ;;
push) shift; cmd_push "$@" ;;
pull) shift; cmd_pull "$@" ;;
screenshot) shift; cmd_screenshot "$@" ;;
info) shift; cmd_info "$@" ;;
*)
# Anything else is a pass-through scrcpy flag for the mirror command.
_require_device
_mirror "$@"
;;
esac