pixelremotecontrol 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +5 -0
- package/README.md +79 -2
- package/assets/phone/pixel-connect +68 -0
- package/assets/phone/pixelremotecontrol +143 -0
- package/assets/viewer/pixelremotecontrol-core.ps1 +207 -0
- package/bin/pixelremotecontrol.js +285 -0
- package/lib/dependencies.js +176 -0
- package/lib/mac-dependencies.js +185 -0
- package/lib/mac-viewer.js +284 -0
- package/package.json +26 -4
- package/scripts/postinstall.js +31 -0
package/DISCLOSURE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Authorized remote device control
|
|
2
|
+
|
|
3
|
+
This package mirrors and controls an Android device that its owner has deliberately paired for ADB Wireless debugging and connected to a trusted SSH relay. It can wake, lock, and restart that device. The phone holder starts sharing from Termux and can stop the session at any time. A viewer must authenticate to the relay and have access through its private network.
|
|
4
|
+
|
|
5
|
+
The commands are intended for development, support, and access to devices owned by the operator or used with the owner's permission. They do not bypass Android's lock screen, ADB pairing, SSH authentication, or Tailscale authorization. The package does not collect credentials or device data.
|
package/README.md
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pixelremotecontrol
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command line launcher for a paired Pixel 8 and a private SSH relay. Use it only
|
|
4
|
+
with a device you own or are authorized to control.
|
|
5
|
+
|
|
6
|
+
On a Windows viewer PC:
|
|
7
|
+
|
|
8
|
+
```powershell
|
|
9
|
+
npm.cmd install -g pixelremotecontrol
|
|
10
|
+
pixelremotecontrol setup --host YOUR_RELAY_TAILSCALE_IP --user YOUR_SSH_USERNAME
|
|
11
|
+
pixelremotecontrol control local pixel8
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`setup` saves the relay address for this viewer and downloads the official
|
|
15
|
+
scrcpy Windows bundle (including `adb`). A later `control` run downloads it if
|
|
16
|
+
needed. Windows OpenSSH Client and Tailscale access to the relay are required.
|
|
17
|
+
SSH authentication is interactive; no password is stored by this package.
|
|
18
|
+
Use `npm.cmd` in PowerShell because Windows execution policy may block
|
|
19
|
+
`npm.ps1`. The `pixelremotecontrol` command uses a `.cmd` launcher and works
|
|
20
|
+
under a Restricted PowerShell execution policy.
|
|
21
|
+
If an older manual installation is still on `PATH`, check which launcher wins
|
|
22
|
+
with `Get-Command pixelremotecontrol -All`. Move the old launcher out of `PATH`
|
|
23
|
+
so the npm `.cmd` is selected.
|
|
24
|
+
|
|
25
|
+
On an Apple Silicon or Intel Mac:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install -g pixelremotecontrol
|
|
29
|
+
pixelremotecontrol setup --host YOUR_RELAY_TAILSCALE_IP --user YOUR_SSH_USERNAME
|
|
30
|
+
pixelremotecontrol control local pixel8
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Mac `setup` downloads the official macOS scrcpy bundle for the Mac's processor,
|
|
34
|
+
verifies its SHA-256 checksum, and caches its `scrcpy` and `adb` executables.
|
|
35
|
+
The Mac needs an SSH client and Tailscale access to the relay. SSH prompts for
|
|
36
|
+
authentication when needed; this package does not save an SSH password.
|
|
37
|
+
`pixelremotecontrol status local pixel8`, `wake`, `lock`, and `restart` also
|
|
38
|
+
work from the Mac. `control asml pixel8` must be run in ASML's interactive
|
|
39
|
+
Windows desktop to put the window there.
|
|
40
|
+
|
|
41
|
+
In Termux on the configured Pixel:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pkg install nodejs npm
|
|
45
|
+
npm install -g pixelremotecontrol
|
|
46
|
+
pixelremotecontrol setup
|
|
47
|
+
pixelremotecontrol startservice pixel8 asml
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If an older standalone `pixelremotecontrol` launcher is already in Termux's
|
|
51
|
+
`$PREFIX/bin`, save it before installing the npm package:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
mv "$PREFIX/bin/pixelremotecontrol" "$PREFIX/bin/pixelremotecontrol.pre-npm"
|
|
55
|
+
npm install -g pixelremotecontrol
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The command installs missing Termux `openssh`, `coreutils`, and `procps`
|
|
59
|
+
packages. It
|
|
60
|
+
uses the phone's existing relay config at `~/.config/pixel-connect/config`
|
|
61
|
+
and key at `~/.ssh/central_tunnel`; it never writes or replaces them. Android
|
|
62
|
+
Wireless debugging and relay ADB pairing must already be enabled. The command
|
|
63
|
+
prompts for the Android connection port when Termux cannot discover it.
|
|
64
|
+
The central machine must already have `pixelctl.cmd` provisioned, its ADB
|
|
65
|
+
service paired with the Pixel, and a restricted SSH tunnel account that the
|
|
66
|
+
phone's key can use. This package cannot complete Tailscale sign-in or
|
|
67
|
+
Android's first-time ADB pairing for you.
|
|
68
|
+
|
|
69
|
+
`pixelremotecontrol wake local pixel8`, `lock`, `restart`, and `status` work on
|
|
70
|
+
Windows and macOS viewers. `control asml pixel8` opens a window on the ASML desktop
|
|
71
|
+
when run there. The current relay scripts support one configured Pixel 8.
|
|
72
|
+
|
|
73
|
+
The mirror can show and control ordinary Android screens, including Settings
|
|
74
|
+
and the lock screen, while Android, Wireless debugging, and the phone's Termux
|
|
75
|
+
relay stay active. Enter the phone's passcode in the scrcpy window when Android
|
|
76
|
+
allows input there; the package does not know or bypass it. Some protected
|
|
77
|
+
screens may be blank. After a reboot, a person must perform the first local
|
|
78
|
+
unlock and restart the Termux service. The bootloader and fastboot screens do
|
|
79
|
+
not run Android or scrcpy, so they require a separate USB host and an external
|
|
80
|
+
way to see the physical display.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/data/data/com.termux/files/usr/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
config="${PIXEL_CONNECT_CONFIG:-$HOME/.config/pixel-connect/config}"
|
|
5
|
+
if [[ ! -r "$config" ]]; then
|
|
6
|
+
printf 'Missing %s. Copy config.example there and set CENTRAL_HOST and CENTRAL_USER.\n' "$config" >&2
|
|
7
|
+
exit 2
|
|
8
|
+
fi
|
|
9
|
+
# The config is owned and edited by the phone's Termux user.
|
|
10
|
+
# shellcheck source=/dev/null
|
|
11
|
+
source "$config"
|
|
12
|
+
|
|
13
|
+
CENTRAL_HOST="${CENTRAL_HOST:-}"
|
|
14
|
+
CENTRAL_USER="${CENTRAL_USER:-}"
|
|
15
|
+
CENTRAL_SSH_PORT="${CENTRAL_SSH_PORT:-22}"
|
|
16
|
+
CENTRAL_ADB_PORT="${CENTRAL_ADB_PORT:-15555}"
|
|
17
|
+
CENTRAL_PAIR_PORT="${CENTRAL_PAIR_PORT:-15556}"
|
|
18
|
+
|
|
19
|
+
usage() {
|
|
20
|
+
cat >&2 <<'USAGE'
|
|
21
|
+
Usage: pixel-connect CONNECT_PORT [PAIR_PORT]
|
|
22
|
+
|
|
23
|
+
Read CONNECT_PORT from Android Settings > Developer options > Wireless debugging
|
|
24
|
+
("IP address & Port"). For first-time pairing, also open "Pair device with
|
|
25
|
+
pairing code" and pass that separate PAIR_PORT. Press Ctrl+C to disconnect.
|
|
26
|
+
USAGE
|
|
27
|
+
exit 2
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
valid_port() {
|
|
31
|
+
[[ "$1" =~ ^[0-9]{1,5}$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 ))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
[[ $# -ge 1 && $# -le 2 ]] || usage
|
|
35
|
+
connect_port="$1"
|
|
36
|
+
valid_port "$connect_port" || usage
|
|
37
|
+
if [[ $# -eq 2 ]]; then
|
|
38
|
+
pair_port="$2"
|
|
39
|
+
valid_port "$pair_port" || usage
|
|
40
|
+
fi
|
|
41
|
+
if [[ -z "$CENTRAL_HOST" || -z "$CENTRAL_USER" ]]; then
|
|
42
|
+
printf 'Set CENTRAL_HOST and CENTRAL_USER in %s.\n' "$config" >&2
|
|
43
|
+
exit 2
|
|
44
|
+
fi
|
|
45
|
+
for port in "$CENTRAL_SSH_PORT" "$CENTRAL_ADB_PORT" "$CENTRAL_PAIR_PORT"; do
|
|
46
|
+
valid_port "$port" || { printf 'Invalid port in %s.\n' "$config" >&2; exit 2; }
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
key="$HOME/.ssh/central_tunnel"
|
|
50
|
+
[[ -r "$key" ]] || { printf 'Missing SSH key: %s\n' "$key" >&2; exit 2; }
|
|
51
|
+
|
|
52
|
+
ssh_args=(
|
|
53
|
+
-N -T -i "$key" -p "$CENTRAL_SSH_PORT"
|
|
54
|
+
-o BatchMode=yes
|
|
55
|
+
-o ExitOnForwardFailure=yes
|
|
56
|
+
-o ServerAliveInterval=20
|
|
57
|
+
-o ServerAliveCountMax=3
|
|
58
|
+
-o StrictHostKeyChecking=yes
|
|
59
|
+
-R "127.0.0.1:${CENTRAL_ADB_PORT}:127.0.0.1:${connect_port}"
|
|
60
|
+
)
|
|
61
|
+
if [[ -n "${pair_port:-}" ]]; then
|
|
62
|
+
ssh_args+=( -R "127.0.0.1:${CENTRAL_PAIR_PORT}:127.0.0.1:${pair_port}" )
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
printf 'Connecting Pixel ADB to %s:%s (central loopback port %s).\n' \
|
|
66
|
+
"$CENTRAL_HOST" "$CENTRAL_SSH_PORT" "$CENTRAL_ADB_PORT"
|
|
67
|
+
printf 'Keep this Termux session open. Press Ctrl+C or run pixel-disconnect to stop sharing.\n'
|
|
68
|
+
exec ssh "${ssh_args[@]}" "${CENTRAL_USER}@${CENTRAL_HOST}"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/data/data/com.termux/files/usr/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
cat >&2 <<'USAGE'
|
|
6
|
+
Usage: pixelremotecontrol startservice <mobiledevice> <destination> [CONNECT_PORT]
|
|
7
|
+
|
|
8
|
+
mobiledevice: pixel8, pixel, thisdevice, or thismachine
|
|
9
|
+
destination: asml (or the host configured for pixel-connect)
|
|
10
|
+
|
|
11
|
+
Run this in Termux on the Pixel. Wireless debugging must be enabled. If its
|
|
12
|
+
connection port cannot be found automatically, this command asks for the
|
|
13
|
+
"IP address & Port" shown in Android's Wireless debugging settings.
|
|
14
|
+
USAGE
|
|
15
|
+
exit 2
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
valid_port() {
|
|
19
|
+
[[ "$1" =~ ^[0-9]{1,5}$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 ))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
port_open() {
|
|
23
|
+
# The reverse tunnel connects to the phone's own loopback ADB listener.
|
|
24
|
+
# A two-second limit avoids hanging if the listener is unavailable.
|
|
25
|
+
timeout 2 bash -c 'exec 3<>"/dev/tcp/127.0.0.1/$1"' _ "$1" >/dev/null 2>&1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
[[ $# -ge 3 && $# -le 4 && "$1" == startservice ]] || usage
|
|
29
|
+
device="${2,,}"
|
|
30
|
+
destination="${3,,}"
|
|
31
|
+
case "$device" in
|
|
32
|
+
pixel8|pixel|thisdevice|thismachine) ;;
|
|
33
|
+
*) printf 'Unknown mobile device: %s\n' "$2" >&2; usage ;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
36
|
+
config="${PIXEL_CONNECT_CONFIG:-$HOME/.config/pixel-connect/config}"
|
|
37
|
+
cache="$(dirname "$config")/last-connect-port"
|
|
38
|
+
if [[ ! -r "$config" ]]; then
|
|
39
|
+
printf 'Missing pixel-connect configuration: %s\n' "$config" >&2
|
|
40
|
+
exit 2
|
|
41
|
+
fi
|
|
42
|
+
# The config is owned and edited by the phone's Termux user.
|
|
43
|
+
# shellcheck source=/dev/null
|
|
44
|
+
source "$config"
|
|
45
|
+
CENTRAL_HOST="${CENTRAL_HOST:-}"
|
|
46
|
+
CENTRAL_USER="${CENTRAL_USER:-}"
|
|
47
|
+
CENTRAL_ADB_PORT="${CENTRAL_ADB_PORT:-15555}"
|
|
48
|
+
if [[ -z "$CENTRAL_HOST" || -z "$CENTRAL_USER" ]] || ! valid_port "$CENTRAL_ADB_PORT"; then
|
|
49
|
+
printf 'Set a valid CENTRAL_HOST, CENTRAL_USER, and CENTRAL_ADB_PORT in %s.\n' "$config" >&2
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
case "$destination" in
|
|
53
|
+
asml|"${CENTRAL_HOST,,}"|"${CENTRAL_USER,,}@${CENTRAL_HOST,,}") ;;
|
|
54
|
+
*) printf 'Destination %s does not match the configured central machine (%s).\n' "$3" "$CENTRAL_HOST" >&2; exit 2 ;;
|
|
55
|
+
esac
|
|
56
|
+
command -v pixel-connect >/dev/null 2>&1 || {
|
|
57
|
+
printf 'pixel-connect is not installed in Termux. Install the Pixel phone tools first.\n' >&2
|
|
58
|
+
exit 2
|
|
59
|
+
}
|
|
60
|
+
command -v timeout >/dev/null 2>&1 || {
|
|
61
|
+
printf 'The Termux coreutils package is required (pkg install coreutils).\n' >&2
|
|
62
|
+
exit 2
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Find only this phone's existing reverse tunnel to the configured machine.
|
|
66
|
+
pattern="^ssh -N -T -i .*/central_tunnel .* -R 127\\.0\\.0\\.1:${CENTRAL_ADB_PORT}:"
|
|
67
|
+
mapfile -t tunnel_pids < <(pgrep -f "$pattern" || true)
|
|
68
|
+
active_port=''
|
|
69
|
+
if ((${#tunnel_pids[@]})); then
|
|
70
|
+
for pid in "${tunnel_pids[@]}"; do
|
|
71
|
+
[[ -r "/proc/$pid/cmdline" ]] || continue
|
|
72
|
+
cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline")"
|
|
73
|
+
[[ "$cmdline" == *" ${CENTRAL_USER}@${CENTRAL_HOST} "* ]] || continue
|
|
74
|
+
if [[ "$cmdline" =~ 127\.0\.0\.1:${CENTRAL_ADB_PORT}:127\.0\.0\.1:([0-9]+) ]]; then
|
|
75
|
+
active_port="${BASH_REMATCH[1]}"
|
|
76
|
+
active_pid="$pid"
|
|
77
|
+
break
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
connect_port=''
|
|
83
|
+
if [[ $# -eq 4 ]]; then
|
|
84
|
+
connect_port="$4"
|
|
85
|
+
elif [[ -n "${PIXEL_CONNECT_PORT:-}" ]]; then
|
|
86
|
+
connect_port="$PIXEL_CONNECT_PORT"
|
|
87
|
+
else
|
|
88
|
+
# This Android system property may be hidden from Termux on some builds.
|
|
89
|
+
if command -v getprop >/dev/null 2>&1; then
|
|
90
|
+
property_port="$(getprop service.adb.tls.port 2>/dev/null || true)"
|
|
91
|
+
if valid_port "$property_port" && port_open "$property_port"; then
|
|
92
|
+
connect_port="$property_port"
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
if [[ -z "$connect_port" && -n "$active_port" ]] && port_open "$active_port"; then
|
|
96
|
+
connect_port="$active_port"
|
|
97
|
+
fi
|
|
98
|
+
if [[ -z "$connect_port" && -r "$cache" ]]; then
|
|
99
|
+
IFS= read -r cached_port < "$cache" || true
|
|
100
|
+
if valid_port "${cached_port:-}" && port_open "$cached_port"; then
|
|
101
|
+
connect_port="$cached_port"
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
if [[ -z "$connect_port" ]]; then
|
|
107
|
+
if [[ ! -t 0 ]]; then
|
|
108
|
+
printf 'Wireless debugging port unknown. Run interactively or pass CONNECT_PORT from Settings > System > Developer options > Wireless debugging > IP address & Port.\n' >&2
|
|
109
|
+
exit 2
|
|
110
|
+
fi
|
|
111
|
+
printf 'Open Settings > System > Developer options > Wireless debugging.\n'
|
|
112
|
+
printf 'Enable Wireless debugging and read the port under "IP address & Port".\n'
|
|
113
|
+
while :; do
|
|
114
|
+
printf 'Connection port: '
|
|
115
|
+
IFS= read -r connect_port || exit 2
|
|
116
|
+
if valid_port "$connect_port" && port_open "$connect_port"; then
|
|
117
|
+
break
|
|
118
|
+
fi
|
|
119
|
+
printf 'That port is invalid or not listening on this phone. Check Wireless debugging and try again.\n' >&2
|
|
120
|
+
done
|
|
121
|
+
elif ! valid_port "$connect_port" || ! port_open "$connect_port"; then
|
|
122
|
+
printf 'Wireless debugging is not listening on port %s. Check its "IP address & Port" in Android Settings.\n' "$connect_port" >&2
|
|
123
|
+
exit 2
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if [[ -n "$active_port" ]]; then
|
|
127
|
+
if [[ "$active_port" == "$connect_port" ]]; then
|
|
128
|
+
printf 'Pixel service is already connected to %s using wireless debugging port %s.\n' "$CENTRAL_HOST" "$connect_port"
|
|
129
|
+
exit 0
|
|
130
|
+
fi
|
|
131
|
+
printf 'Replacing the stale Pixel tunnel (port %s) with port %s.\n' "$active_port" "$connect_port"
|
|
132
|
+
kill "$active_pid" 2>/dev/null || true
|
|
133
|
+
# Give sshd time to release the existing reverse-forwarded port.
|
|
134
|
+
for ((attempt=0; attempt<20; attempt++)); do
|
|
135
|
+
kill -0 "$active_pid" 2>/dev/null || break
|
|
136
|
+
sleep 0.1
|
|
137
|
+
done
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
mkdir -p "$(dirname "$cache")"
|
|
141
|
+
umask 077
|
|
142
|
+
printf '%s\n' "$connect_port" > "$cache"
|
|
143
|
+
exec pixel-connect "$connect_port"
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[Parameter(Position = 0)]
|
|
4
|
+
[string]$Action = 'control',
|
|
5
|
+
[Parameter(Position = 1)]
|
|
6
|
+
[string]$Display = 'local',
|
|
7
|
+
[Parameter(Position = 2)]
|
|
8
|
+
[string]$DeviceId = 'pixel8',
|
|
9
|
+
[string]$CentralHost = '',
|
|
10
|
+
[string]$CentralUser = '',
|
|
11
|
+
[ValidateRange(1, 65535)]
|
|
12
|
+
[int]$SshPort = 22,
|
|
13
|
+
[ValidateRange(1024, 65535)]
|
|
14
|
+
[int]$AdbForwardPort = 5038,
|
|
15
|
+
[ValidateRange(1024, 65535)]
|
|
16
|
+
[int]$VideoPort = 27183,
|
|
17
|
+
[string]$ScrcpyPath = '',
|
|
18
|
+
[string]$AdbPath = '',
|
|
19
|
+
[ValidateRange(0, 86400)]
|
|
20
|
+
[int]$TimeLimitSeconds = 0,
|
|
21
|
+
[switch]$Help
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
$ErrorActionPreference = 'Stop'
|
|
25
|
+
|
|
26
|
+
function Show-Usage {
|
|
27
|
+
Write-Host 'Usage: pixelremotecontrol control <local|asml> <pixel8|Pixel_8> [options]'
|
|
28
|
+
Write-Host ' pixelremotecontrol <wake|lock|restart|status> <local|asml> <pixel8|Pixel_8>'
|
|
29
|
+
Write-Host 'Options: -CentralHost HOST -CentralUser USER -SshPort PORT -ScrcpyPath PATH -AdbPath PATH'
|
|
30
|
+
Write-Host 'Run "control local" on a Windows viewer to see and control the Pixel here.'
|
|
31
|
+
Write-Host 'Run "control asml" only in an interactive desktop session on ASML.'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function Resolve-Program {
|
|
35
|
+
param([string]$Name, [string]$ExplicitPath, [string]$SiblingPath)
|
|
36
|
+
if ($ExplicitPath) {
|
|
37
|
+
if (-not (Test-Path -LiteralPath $ExplicitPath -PathType Leaf)) {
|
|
38
|
+
throw "$Name was not found at '$ExplicitPath'."
|
|
39
|
+
}
|
|
40
|
+
return (Resolve-Path -LiteralPath $ExplicitPath).Path
|
|
41
|
+
}
|
|
42
|
+
if ($SiblingPath -and (Test-Path -LiteralPath $SiblingPath -PathType Leaf)) {
|
|
43
|
+
return (Resolve-Path -LiteralPath $SiblingPath).Path
|
|
44
|
+
}
|
|
45
|
+
$besideScript = Join-Path $PSScriptRoot $Name
|
|
46
|
+
if (Test-Path -LiteralPath $besideScript -PathType Leaf) {
|
|
47
|
+
return (Resolve-Path -LiteralPath $besideScript).Path
|
|
48
|
+
}
|
|
49
|
+
$command = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
50
|
+
if ($command) { return $command.Source }
|
|
51
|
+
if ($env:LOCALAPPDATA) {
|
|
52
|
+
$packages = Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages'
|
|
53
|
+
if (Test-Path -LiteralPath $packages -PathType Container) {
|
|
54
|
+
$found = Get-ChildItem -LiteralPath $packages -Directory -Filter 'Genymobile.scrcpy*' -ErrorAction SilentlyContinue |
|
|
55
|
+
ForEach-Object { Get-ChildItem -LiteralPath $_.FullName -Directory -Filter 'scrcpy-win64-*' -ErrorAction SilentlyContinue } |
|
|
56
|
+
Sort-Object Name -Descending |
|
|
57
|
+
ForEach-Object { Join-Path $_.FullName $Name } |
|
|
58
|
+
Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
|
|
59
|
+
Select-Object -First 1
|
|
60
|
+
if ($found) { return $found }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
throw "$Name was not found. Extract the scrcpy Windows ZIP or pass the corresponding path option."
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Test-PortFree {
|
|
67
|
+
param([int]$Port)
|
|
68
|
+
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $Port)
|
|
69
|
+
try { $listener.Start() }
|
|
70
|
+
catch { throw "Local port $Port is already in use. Close the other viewer or choose another forward port." }
|
|
71
|
+
finally { $listener.Stop() }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function Test-PortOpen {
|
|
75
|
+
param([int]$Port)
|
|
76
|
+
$client = New-Object System.Net.Sockets.TcpClient
|
|
77
|
+
try {
|
|
78
|
+
$pending = $client.BeginConnect('127.0.0.1', $Port, $null, $null)
|
|
79
|
+
if (-not $pending.AsyncWaitHandle.WaitOne(200)) { return $false }
|
|
80
|
+
$client.EndConnect($pending)
|
|
81
|
+
return $true
|
|
82
|
+
} catch { return $false }
|
|
83
|
+
finally { $client.Dispose() }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Invoke-Adb {
|
|
87
|
+
param([string]$Program, [string[]]$Arguments)
|
|
88
|
+
# Windows PowerShell 5.1 may turn a native stderr line into a terminating
|
|
89
|
+
# NativeCommandError when ErrorActionPreference is Stop.
|
|
90
|
+
$oldPreference = $ErrorActionPreference
|
|
91
|
+
$ErrorActionPreference = 'Continue'
|
|
92
|
+
try {
|
|
93
|
+
$output = @(& $Program @Arguments 2>&1 | ForEach-Object { $_.ToString() })
|
|
94
|
+
$exitCode = $LASTEXITCODE
|
|
95
|
+
return [pscustomobject]@{ Output = $output; ExitCode = $exitCode }
|
|
96
|
+
} finally { $ErrorActionPreference = $oldPreference }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
if ($Help -or $Action -in @('help', '--help', '-help', '/?')) { Show-Usage; exit 0 }
|
|
101
|
+
|
|
102
|
+
if ($Display -eq 'thismachine') { $Display = 'local' }
|
|
103
|
+
if ($Action -notin @('control', 'wake', 'lock', 'restart', 'status')) {
|
|
104
|
+
throw "Unknown action '$Action'. Run pixelremotecontrol --help."
|
|
105
|
+
}
|
|
106
|
+
if ($Display -notin @('local', 'asml')) {
|
|
107
|
+
throw "Unknown display '$Display'. Use local or asml."
|
|
108
|
+
}
|
|
109
|
+
if ($DeviceId -notin @('pixel8', 'pixel_8', 'pixel-8', '127.0.0.1:15555')) {
|
|
110
|
+
throw "Unknown device '$DeviceId'. This relay currently has one registered device: pixel8 (Pixel_8)."
|
|
111
|
+
}
|
|
112
|
+
if ($CentralUser -notmatch '^[A-Za-z0-9._-]+$') { throw 'CentralUser contains invalid SSH username characters.' }
|
|
113
|
+
if ($CentralHost -notmatch '^[A-Za-z0-9.:-]+$') { throw 'CentralHost contains invalid SSH host characters.' }
|
|
114
|
+
if ($AdbForwardPort -eq $VideoPort) { throw 'AdbForwardPort and VideoPort must differ.' }
|
|
115
|
+
|
|
116
|
+
$ssh = (Get-Command ssh.exe -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
|
|
117
|
+
$destination = "$CentralUser@$CentralHost"
|
|
118
|
+
|
|
119
|
+
if ($Action -ne 'control') {
|
|
120
|
+
$remoteArgs = @('-T', '-p', [string]$SshPort, $destination, "pixelctl.cmd $Action")
|
|
121
|
+
$remote = Start-Process -FilePath $ssh -ArgumentList $remoteArgs -NoNewWindow -PassThru -Wait
|
|
122
|
+
exit $remote.ExitCode
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if ($Display -eq 'asml') {
|
|
126
|
+
if ($env:COMPUTERNAME -ine 'ASML' -or -not [Environment]::UserInteractive) {
|
|
127
|
+
throw 'ASML screen mode needs an interactive desktop login on ASML. Use "control local pixel8" here, or open an ASML desktop through Remote Desktop.'
|
|
128
|
+
}
|
|
129
|
+
$pixelctl = Join-Path $env:USERPROFILE 'pixelctl.cmd'
|
|
130
|
+
if (-not (Test-Path -LiteralPath $pixelctl -PathType Leaf)) {
|
|
131
|
+
throw "pixelctl.cmd was not found in $env:USERPROFILE."
|
|
132
|
+
}
|
|
133
|
+
& $pixelctl view
|
|
134
|
+
exit $LASTEXITCODE
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
$scrcpy = Resolve-Program -Name 'scrcpy.exe' -ExplicitPath $ScrcpyPath
|
|
138
|
+
$adb = Resolve-Program -Name 'adb.exe' -ExplicitPath $AdbPath -SiblingPath (Join-Path (Split-Path -Parent $scrcpy) 'adb.exe')
|
|
139
|
+
Test-PortFree -Port $AdbForwardPort
|
|
140
|
+
Test-PortFree -Port $VideoPort
|
|
141
|
+
|
|
142
|
+
$previousAdbSocket = $env:ADB_SERVER_SOCKET
|
|
143
|
+
$previousAdb = $env:ADB
|
|
144
|
+
$sshProcess = $null
|
|
145
|
+
try {
|
|
146
|
+
$sshArgs = @(
|
|
147
|
+
'-T', '-o', 'ExitOnForwardFailure=yes', '-o', 'ServerAliveInterval=15',
|
|
148
|
+
'-o', 'ServerAliveCountMax=3', '-p', [string]$SshPort,
|
|
149
|
+
'-L', "127.0.0.1:${AdbForwardPort}:127.0.0.1:5039",
|
|
150
|
+
'-L', "127.0.0.1:${VideoPort}:127.0.0.1:${VideoPort}",
|
|
151
|
+
$destination, 'pixelctl.cmd serve'
|
|
152
|
+
)
|
|
153
|
+
Write-Host "Opening SSH to $destination. Complete any host key or password prompt in this window."
|
|
154
|
+
$sshProcess = Start-Process -FilePath $ssh -ArgumentList $sshArgs -NoNewWindow -PassThru
|
|
155
|
+
|
|
156
|
+
$tunnelReady = $false
|
|
157
|
+
for ($attempt = 0; $attempt -lt 600; $attempt++) {
|
|
158
|
+
if ($sshProcess.HasExited) { throw "SSH exited with code $($sshProcess.ExitCode). Check the address, login, host key, and password." }
|
|
159
|
+
if (Test-PortOpen -Port $AdbForwardPort) { $tunnelReady = $true; break }
|
|
160
|
+
Start-Sleep -Milliseconds 200
|
|
161
|
+
}
|
|
162
|
+
if (-not $tunnelReady) { throw 'SSH forwarding did not open within two minutes.' }
|
|
163
|
+
|
|
164
|
+
$env:ADB_SERVER_SOCKET = "tcp:127.0.0.1:$AdbForwardPort"
|
|
165
|
+
$env:ADB = $adb
|
|
166
|
+
$serial = '127.0.0.1:15555'
|
|
167
|
+
$deviceReady = $false
|
|
168
|
+
$lastMessage = ''
|
|
169
|
+
for ($attempt = 0; $attempt -lt 60; $attempt++) {
|
|
170
|
+
if ($sshProcess.HasExited) { throw "SSH ended while waiting for the Pixel (exit $($sshProcess.ExitCode))." }
|
|
171
|
+
$connect = Invoke-Adb -Program $adb -Arguments @('connect', $serial)
|
|
172
|
+
$lastMessage = ($connect.Output -join ' ')
|
|
173
|
+
if ($connect.ExitCode -eq 0) {
|
|
174
|
+
$devices = Invoke-Adb -Program $adb -Arguments @('devices', '-l')
|
|
175
|
+
if ($devices.ExitCode -eq 0 -and (($devices.Output -join "`n") -match '(?m)^127\.0\.0\.1:15555\s+device\b')) {
|
|
176
|
+
$deviceReady = $true
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
$lastMessage = ($devices.Output -join ' ')
|
|
180
|
+
}
|
|
181
|
+
Start-Sleep -Seconds 1
|
|
182
|
+
}
|
|
183
|
+
if (-not $deviceReady) {
|
|
184
|
+
throw "The Pixel did not become available through ASML. Ask the holder to run 'pixelremotecontrol startservice pixel8 asml' in Termux. $lastMessage"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
Write-Host 'Pixel connected. Opening screen control; close the scrcpy window to end this session.'
|
|
188
|
+
$scrcpyArgs = @('--force-adb-forward', "--port=$VideoPort", '--serial', $serial)
|
|
189
|
+
if ($TimeLimitSeconds -gt 0) { $scrcpyArgs += "--time-limit=$TimeLimitSeconds" }
|
|
190
|
+
$oldPreference = $ErrorActionPreference
|
|
191
|
+
$ErrorActionPreference = 'Continue'
|
|
192
|
+
try {
|
|
193
|
+
& $scrcpy @scrcpyArgs
|
|
194
|
+
$scrcpyExit = $LASTEXITCODE
|
|
195
|
+
} finally { $ErrorActionPreference = $oldPreference }
|
|
196
|
+
if ($scrcpyExit -ne 0) { throw "scrcpy exited with code $scrcpyExit." }
|
|
197
|
+
} finally {
|
|
198
|
+
$env:ADB_SERVER_SOCKET = $previousAdbSocket
|
|
199
|
+
$env:ADB = $previousAdb
|
|
200
|
+
if ($sshProcess -and -not $sshProcess.HasExited) {
|
|
201
|
+
Stop-Process -Id $sshProcess.Id -Force -ErrorAction SilentlyContinue
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
[Console]::Error.WriteLine("pixelremotecontrol: $($_.Exception.Message)")
|
|
206
|
+
exit 1
|
|
207
|
+
}
|