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 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
- # Temporary Holding Version
1
+ # pixelremotecontrol
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ }