mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 05:02:50 +02:00
Fix review findings and Windows setup issues found in testing
- Run the server with -X utf8: the bundled Windows Python ignores PYTHON* variables. - Quote every argument in Windows terminal commands, so cmd metacharacters are literal. - frame_connect: accept HOST:PORT, validate input, retry the config swap while Windows' ssh.exe holds ~/.ssh/config locked, and don't apply 0o700 on Windows. - Never use rsync on Windows; unbounded stream queue; validate FRAME_ALIAS; more Linux terminals; bundle the window icon; docs and wording fixes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
fc2fa65f0d
commit
e210407f31
11 files changed
+90
-39
No files matched your search
@@ -93,7 +93,7 @@ already ships. [How each feature works](docs/frame-control.md).
|
|||||||
| | Download | Needs |
|
| | Download | Needs |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **macOS** (Apple Silicon) | [Frame-Control-mac-arm64.dmg](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-mac-arm64.dmg) | Python 3 (`xcode-select --install`) |
|
| **macOS** (Apple Silicon) | [Frame-Control-mac-arm64.dmg](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-mac-arm64.dmg) | Python 3 (`xcode-select --install`) |
|
||||||
| **Windows** 10 / 11 (x64) | [Frame-Control-Setup-x64.exe](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-Setup-x64.exe) · [portable .zip](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-win-x64.zip) | Nothing extra: Python is bundled |
|
| **Windows** 10 / 11 (x64) | [Frame-Control-Setup-x64.exe](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-Setup-x64.exe) · [portable .zip](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-win-x64.zip) | Nothing extra: Python is bundled, and SSH is built into Windows |
|
||||||
| **Linux** (x64) | [AppImage](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-x86_64.AppImage) · [.deb](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-amd64.deb) | `python3` and `ssh` (most desktops have both) |
|
| **Linux** (x64) | [AppImage](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-x86_64.AppImage) · [.deb](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-amd64.deb) | `python3` and `ssh` (most desktops have both) |
|
||||||
| **Linux** (arm64) | [AppImage](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-arm64.AppImage) · [.deb](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-arm64.deb) | same |
|
| **Linux** (arm64) | [AppImage](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-arm64.AppImage) · [.deb](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-arm64.deb) | same |
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ function get(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
if (fs.existsSync(path.join(OUT, "python.exe")) && fs.readFileSync(path.join(OUT, ".version"), "utf8") === VERSION) {
|
const stamp = path.join(OUT, ".version");
|
||||||
|
if (fs.existsSync(path.join(OUT, "python.exe")) && fs.existsSync(stamp) && fs.readFileSync(stamp, "utf8") === VERSION) {
|
||||||
console.log(`Python ${VERSION} already in ${OUT}`);
|
console.log(`Python ${VERSION} already in ${OUT}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -109,7 +109,7 @@ function ping(target) {
|
|||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
const env = { ...process.env, PATH: await loginPath(), PYTHONUNBUFFERED: "1", PYTHONDONTWRITEBYTECODE: "1",
|
const env = { ...process.env, PATH: await loginPath(), PYTHONUNBUFFERED: "1", PYTHONDONTWRITEBYTECODE: "1",
|
||||||
PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", FRAME_CONTROL_APP: "1" }; // UTF-8 even on Windows
|
PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", FRAME_CONTROL_APP: "1" };
|
||||||
python = await findPython(env);
|
python = await findPython(env);
|
||||||
if (!python) throw new Error(`Frame Control needs Python 3.8 or later. ${PYTHON_HELP}`);
|
if (!python) throw new Error(`Frame Control needs Python 3.8 or later. ${PYTHON_HELP}`);
|
||||||
if (!await hasSsh(env)) throw new Error(`Frame Control needs the ssh command. ${SSH_HELP}`);
|
if (!await hasSsh(env)) throw new Error(`Frame Control needs the ssh command. ${SSH_HELP}`);
|
||||||
@@ -118,7 +118,8 @@ async function startServer() {
|
|||||||
const log = fs.openSync(LOG, "a");
|
const log = fs.openSync(LOG, "a");
|
||||||
fs.writeSync(log, `\n--- ${new Date().toISOString()} ${python} ${SERVER} --port ${port}\n`);
|
fs.writeSync(log, `\n--- ${new Date().toISOString()} ${python} ${SERVER} --port ${port}\n`);
|
||||||
// stdin stays open while the app runs; the server exits cleanly when it closes.
|
// stdin stays open while the app runs; the server exits cleanly when it closes.
|
||||||
const child = spawn(python, [SERVER, "--port", String(port), "--exit-on-eof"],
|
// -X utf8: the bundled Windows Python ignores PYTHON* variables (isolated mode).
|
||||||
|
const child = spawn(python, ["-X", "utf8", SERVER, "--port", String(port), "--exit-on-eof"],
|
||||||
{ env, stdio: ["pipe", log, log], windowsHide: true });
|
{ env, stdio: ["pipe", log, log], windowsHide: true });
|
||||||
child.stdin.on("error", () => {});
|
child.stdin.on("error", () => {});
|
||||||
fs.closeSync(log);
|
fs.closeSync(log);
|
||||||
|
|||||||
+2
-1
@@ -27,7 +27,8 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"main.js",
|
"main.js",
|
||||||
"package.json"
|
"package.json",
|
||||||
|
"build/icon.png"
|
||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ As of 2026-09-25 no other desktop app manages the Frame end to end.
|
|||||||
the headset over SSH. [FrameDrop](https://framedropvr.com) sideloads but is
|
the headset over SSH. [FrameDrop](https://framedropvr.com) sideloads but is
|
||||||
Windows-only. Steam Link views the headset.
|
Windows-only. Steam Link views the headset.
|
||||||
|
|
||||||
You can also run the same UI in a browser without the app, from a checkout on
|
You can also run the same UI in a browser without the app, from a checkout:
|
||||||
macOS or Linux:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./scripts/frame-ui.sh # opens http://127.0.0.1:47810 in its own window
|
./scripts/frame-ui.sh # macOS: opens http://127.0.0.1:47810 in its own window
|
||||||
|
python3 ui/server.py # anywhere: then open http://127.0.0.1:47810
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
+3
-2
@@ -108,7 +108,8 @@ _install_lock = threading.Lock() # installs are rare; one at a time avoids ever
|
|||||||
def _copy(src, dest, executable=False, timeout=600):
|
def _copy(src, dest, executable=False, timeout=600):
|
||||||
"""Copy a local file to the Frame: rsync where installed (not on Windows), else scp."""
|
"""Copy a local file to the Frame: rsync where installed (not on Windows), else scp."""
|
||||||
name = os.path.basename(src)
|
name = os.path.basename(src)
|
||||||
if shutil.which('rsync'):
|
rsync = None if frame_host.WINDOWS else shutil.which('rsync') # see server.push_file
|
||||||
|
if rsync:
|
||||||
cmd = ['rsync', '-a', *(['--chmod=u+x'] if executable else []),
|
cmd = ['rsync', '-a', *(['--chmod=u+x'] if executable else []),
|
||||||
'-e', shlex.join(['ssh', *SSH_OPTS]), src, f'{FRAME}:{dest}']
|
'-e', shlex.join(['ssh', *SSH_OPTS]), src, f'{FRAME}:{dest}']
|
||||||
else:
|
else:
|
||||||
@@ -119,7 +120,7 @@ def _copy(src, dest, executable=False, timeout=600):
|
|||||||
raise FrameError(f'copying {name} to the Frame timed out')
|
raise FrameError(f'copying {name} to the Frame timed out')
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
raise FrameError(f'copying {name} to the Frame failed: {(e.stderr or "").strip()[-300:]}')
|
raise FrameError(f'copying {name} to the Frame failed: {(e.stderr or "").strip()[-300:]}')
|
||||||
if executable and not shutil.which('rsync'):
|
if executable and not rsync:
|
||||||
ssh(f'chmod u+x {shlex.quote(dest)}')
|
ssh(f'chmod u+x {shlex.quote(dest)}')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ def key():
|
|||||||
p = subprocess.run(['security', 'find-generic-password', '-s', KEYCHAIN[0], '-a', KEYCHAIN[1], '-w'],
|
p = subprocess.run(['security', 'find-generic-password', '-s', KEYCHAIN[0], '-a', KEYCHAIN[1], '-w'],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True)
|
||||||
if p is None or p.returncode != 0 or not p.stdout.strip():
|
if p is None or p.returncode != 0 or not p.stdout.strip():
|
||||||
raise DBError('No compatibility-database key in the Keychain '
|
raise DBError('No compatibility-database key (set FRAME_CONTROL_KEY, or on macOS the Keychain '
|
||||||
f'(service {KEYCHAIN[0]}, account {KEYCHAIN[1]})')
|
f'item service {KEYCHAIN[0]}, account {KEYCHAIN[1]})')
|
||||||
return p.stdout.strip()
|
return p.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+61
-19
@@ -3,15 +3,17 @@ alias to ~/.ssh/config and copy the key over, asking for the Developer Mode
|
|||||||
password once. The Linux and Windows twin of scripts/connect.sh (which the Mac
|
password once. The Linux and Windows twin of scripts/connect.sh (which the Mac
|
||||||
app uses); same config block, so either can re-run over the other. Idempotent.
|
app uses); same config block, so either can re-run over the other. Idempotent.
|
||||||
|
|
||||||
Usage: python3 ui/frame_connect.py [HOST_OR_IP]
|
Usage: python3 ui/frame_connect.py [HOST_OR_IP[:PORT]]
|
||||||
Env: FRAME_USER (default steamos), FRAME_ALIAS (default frame)
|
Env: FRAME_USER (default steamos), FRAME_ALIAS (default frame)
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
FRAME_USER = os.environ.get("FRAME_USER", "steamos")
|
FRAME_USER = os.environ.get("FRAME_USER", "steamos")
|
||||||
@@ -36,26 +38,50 @@ def say(msg):
|
|||||||
print(msg, flush=True)
|
print(msg, flush=True)
|
||||||
|
|
||||||
|
|
||||||
def port_open(host):
|
def split_port(arg):
|
||||||
|
""""host:2222" -> ("host", 2222); anything else (IPv6 too) keeps port 22."""
|
||||||
|
host, sep, port = arg.rpartition(":")
|
||||||
|
if sep and port.isdigit() and ":" not in host:
|
||||||
|
return host, int(port)
|
||||||
|
return arg, 22
|
||||||
|
|
||||||
|
|
||||||
|
def port_open(host, port=22):
|
||||||
try:
|
try:
|
||||||
with socket.create_connection((host, 22), timeout=3):
|
with socket.create_connection((host, port), timeout=3):
|
||||||
return True
|
return True
|
||||||
except OSError:
|
except OSError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
HOST_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9.:%-]*")
|
||||||
|
|
||||||
|
|
||||||
def pick_host(arg):
|
def pick_host(arg):
|
||||||
for host in [arg] if arg else [f"{FRAME_ALIAS}.local", FRAME_ALIAS]:
|
if arg and not HOST_RE.fullmatch(arg):
|
||||||
if port_open(host):
|
say(f" - {arg!r} isn't a host name or IP address")
|
||||||
return host
|
return None
|
||||||
say(f" - {host}: not resolvable or port 22 closed")
|
for cand in [arg] if arg else [f"{FRAME_ALIAS}.local", FRAME_ALIAS]:
|
||||||
|
host, port = split_port(cand)
|
||||||
|
if port_open(host, port):
|
||||||
|
return host, port
|
||||||
|
say(f" - {cand}: not resolvable or port {port} closed")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def write_config(host):
|
def make_ssh_dir():
|
||||||
|
# Windows: no mode. Python 3.12.4+ turns 0o700 into an owner-only ACL, which locks
|
||||||
|
# the user out if the folder's owner is Administrators; the profile's ACL suffices.
|
||||||
|
if os.name == "nt":
|
||||||
|
SSH_DIR.mkdir(exist_ok=True)
|
||||||
|
else:
|
||||||
|
SSH_DIR.mkdir(mode=0o700, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def write_config(host, port=22):
|
||||||
"""Replace our managed block and put it first: ssh uses the first value it sees per
|
"""Replace our managed block and put it first: ssh uses the first value it sees per
|
||||||
option. The trailing "Host *" returns the rest of the file to global scope."""
|
option. The trailing "Host *" returns the rest of the file to global scope."""
|
||||||
SSH_DIR.mkdir(mode=0o700, exist_ok=True)
|
make_ssh_dir()
|
||||||
old = CONFIG.read_text(encoding="utf-8") if CONFIG.exists() else ""
|
old = CONFIG.read_text(encoding="utf-8") if CONFIG.exists() else ""
|
||||||
kept, skip = [], False
|
kept, skip = [], False
|
||||||
for line in old.splitlines():
|
for line in old.splitlines():
|
||||||
@@ -65,12 +91,27 @@ def write_config(host):
|
|||||||
skip = False
|
skip = False
|
||||||
elif not skip:
|
elif not skip:
|
||||||
kept.append(line)
|
kept.append(line)
|
||||||
block = [BEGIN, f"Host {FRAME_ALIAS}", f" HostName {host}", f" User {FRAME_USER}",
|
block = [BEGIN, f"Host {FRAME_ALIAS}", f" HostName {host}", *([f" Port {port}"] if port != 22 else []),
|
||||||
|
f" User {FRAME_USER}",
|
||||||
" IdentityFile ~/.ssh/id_ed25519_frame", " IdentitiesOnly yes",
|
" IdentityFile ~/.ssh/id_ed25519_frame", " IdentitiesOnly yes",
|
||||||
" ServerAliveInterval 30", "Host *", END]
|
" ServerAliveInterval 30", "Host *", END]
|
||||||
CONFIG.write_text("\n".join(block + kept) + "\n", encoding="utf-8")
|
tmp = CONFIG.with_name("config.frame-control.tmp")
|
||||||
|
tmp.write_text("\n".join(block + kept) + "\n", encoding="utf-8")
|
||||||
if os.name != "nt":
|
if os.name != "nt":
|
||||||
CONFIG.chmod(0o600)
|
tmp.chmod(0o600)
|
||||||
|
# On Windows a running ssh.exe (Frame Control's own, say) keeps the config open
|
||||||
|
# and locked, so the swap can fail for a moment; keep trying for a while.
|
||||||
|
for attempt in range(60):
|
||||||
|
try:
|
||||||
|
os.replace(tmp, CONFIG)
|
||||||
|
return
|
||||||
|
except PermissionError:
|
||||||
|
if attempt == 0:
|
||||||
|
say(" ~/.ssh/config is in use by another ssh; waiting for it...")
|
||||||
|
time.sleep(0.5)
|
||||||
|
tmp.unlink(missing_ok=True)
|
||||||
|
raise SystemExit("~/.ssh/config stayed locked by another program. Quit Frame Control "
|
||||||
|
"and any ssh windows, then run the setup again.")
|
||||||
|
|
||||||
|
|
||||||
def key_login_works():
|
def key_login_works():
|
||||||
@@ -82,8 +123,8 @@ def main(argv):
|
|||||||
if argv and argv[0] in ("-h", "--help"):
|
if argv and argv[0] in ("-h", "--help"):
|
||||||
sys.exit(__doc__)
|
sys.exit(__doc__)
|
||||||
say("==> Looking for the Steam Frame")
|
say("==> Looking for the Steam Frame")
|
||||||
host = pick_host(argv[0] if argv else None)
|
found = pick_host(argv[0] if argv else None)
|
||||||
while not host:
|
while not found:
|
||||||
say("Could not reach the Frame on port 22.")
|
say("Could not reach the Frame on port 22.")
|
||||||
say("Check: Developer Mode on and a user password set; same network; no client isolation.")
|
say("Check: Developer Mode on and a user password set; same network; no client isolation.")
|
||||||
try:
|
try:
|
||||||
@@ -92,11 +133,12 @@ def main(argv):
|
|||||||
typed = ""
|
typed = ""
|
||||||
if not typed:
|
if not typed:
|
||||||
return 1
|
return 1
|
||||||
host = pick_host(typed)
|
found = pick_host(typed)
|
||||||
say(f" found: {host}")
|
host, port = found
|
||||||
|
say(f" found: {host}" + (f" port {port}" if port != 22 else ""))
|
||||||
|
|
||||||
say("==> SSH key")
|
say("==> SSH key")
|
||||||
SSH_DIR.mkdir(mode=0o700, exist_ok=True)
|
make_ssh_dir()
|
||||||
if KEY.exists():
|
if KEY.exists():
|
||||||
say(f" exists: {KEY}")
|
say(f" exists: {KEY}")
|
||||||
else:
|
else:
|
||||||
@@ -105,7 +147,7 @@ def main(argv):
|
|||||||
say(f" created {KEY}")
|
say(f" created {KEY}")
|
||||||
|
|
||||||
say(f"==> ~/.ssh/config alias '{FRAME_ALIAS}' -> {host}")
|
say(f"==> ~/.ssh/config alias '{FRAME_ALIAS}' -> {host}")
|
||||||
write_config(host)
|
write_config(host, port)
|
||||||
|
|
||||||
say("==> Checking key login")
|
say("==> Checking key login")
|
||||||
if key_login_works():
|
if key_login_works():
|
||||||
@@ -114,7 +156,7 @@ def main(argv):
|
|||||||
say(" copying the key: enter the Developer Mode password when asked")
|
say(" copying the key: enter the Developer Mode password when asked")
|
||||||
pub = KEY.with_suffix(".pub").read_text(encoding="utf-8").strip()
|
pub = KEY.with_suffix(".pub").read_text(encoding="utf-8").strip()
|
||||||
r = subprocess.run(["ssh", "-o", "StrictHostKeyChecking=accept-new", "-o", "PubkeyAuthentication=no",
|
r = subprocess.run(["ssh", "-o", "StrictHostKeyChecking=accept-new", "-o", "PubkeyAuthentication=no",
|
||||||
f"{FRAME_USER}@{host}", ADD_KEY_CMD], input=pub + "\n", text=True)
|
"-p", str(port), f"{FRAME_USER}@{host}", ADD_KEY_CMD], input=pub + "\n", text=True)
|
||||||
if r.returncode != 0 or not key_login_works():
|
if r.returncode != 0 or not key_login_works():
|
||||||
say("Key login still isn't working. Check the password and run this again.")
|
say("Key login still isn't working. Check the password and run this again.")
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
+7
-5
@@ -146,16 +146,18 @@ def open_terminal(argv, title="Frame Control"):
|
|||||||
# `start` gives the command its own console window; cmd /k keeps it open.
|
# `start` gives the command its own console window; cmd /k keeps it open.
|
||||||
# One hand-built command line: quoting it twice through list2cmdline would
|
# One hand-built command line: quoting it twice through list2cmdline would
|
||||||
# produce backslash-escaped quotes, which cmd doesn't understand.
|
# produce backslash-escaped quotes, which cmd doesn't understand.
|
||||||
inner = subprocess.list2cmdline(argv)
|
# Every argument is quoted, so cmd treats & | < > ^ in them literally.
|
||||||
|
inner = " ".join('"%s"' % a.replace('"', '\\"') for a in argv)
|
||||||
subprocess.Popen(f'cmd.exe /c start "{title}" cmd.exe /k "{inner}"', **DETACHED)
|
subprocess.Popen(f'cmd.exe /c start "{title}" cmd.exe /k "{inner}"', **DETACHED)
|
||||||
return "a terminal window"
|
return "a terminal window"
|
||||||
script = f'{shlex.join(argv)}; echo; read -r -p "Press Enter to close. " _'
|
script = f'{shlex.join(argv)}; echo; read -r -p "Press Enter to close. " _'
|
||||||
for name, flag in (("x-terminal-emulator", "-e"), ("gnome-terminal", "--"), ("konsole", "-e"),
|
for name, flags in (("x-terminal-emulator", ["-e"]), ("gnome-terminal", ["--"]), ("ptyxis", ["--"]),
|
||||||
("xfce4-terminal", "-x"), ("kitty", None), ("alacritty", "-e"),
|
("kgx", ["--"]), ("konsole", ["-e"]), ("xfce4-terminal", ["-x"]),
|
||||||
("foot", None), ("xterm", "-e")):
|
("tilix", ["-e"]), ("lxterminal", ["-e"]), ("kitty", []), ("alacritty", ["-e"]),
|
||||||
|
("wezterm", ["start", "--"]), ("foot", []), ("xterm", ["-e"])):
|
||||||
exe = which(name)
|
exe = which(name)
|
||||||
if exe:
|
if exe:
|
||||||
_spawn([exe, *([flag] if flag else []), "bash", "-c", script])
|
_spawn([exe, *flags, "bash", "-c", script])
|
||||||
return name
|
return name
|
||||||
raise HostError("No terminal program found (tried gnome-terminal, konsole, xterm and others)")
|
raise HostError("No terminal program found (tried gnome-terminal, konsole, xterm and others)")
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1100,7 +1100,7 @@ function upload(file, mode) {
|
|||||||
}
|
}
|
||||||
async function sendFiles(files) {
|
async function sendFiles(files) {
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
if (!f.size) { toast(`${f.name}: folders and empty files aren't supported here; use scripts/push.sh`, true); continue; }
|
if (!f.size) { toast(`${f.name}: folders and empty files aren't supported here; zip the folder first`, true); continue; }
|
||||||
const apk = f.name.toLowerCase().endsWith(".apk");
|
const apk = f.name.toLowerCase().endsWith(".apk");
|
||||||
await act(apk ? `Install ${f.name}` : `Copy ${f.name} to ~/Downloads`, () => upload(f, apk ? "apk" : "push"));
|
await act(apk ? `Install ${f.name}` : `Copy ${f.name} to ~/Downloads`, () => upload(f, apk ? "apk" : "push"));
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -39,6 +39,8 @@ import frame_store # noqa: E402
|
|||||||
|
|
||||||
HERE = Path(__file__).resolve().parent
|
HERE = Path(__file__).resolve().parent
|
||||||
FRAME = os.environ.get("FRAME_ALIAS", "frame")
|
FRAME = os.environ.get("FRAME_ALIAS", "frame")
|
||||||
|
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", FRAME):
|
||||||
|
sys.exit(f"FRAME_ALIAS must be a plain host alias, not {FRAME!r}")
|
||||||
# Reuse one SSH connection for the frequent status/screenshot calls, where ssh
|
# Reuse one SSH connection for the frequent status/screenshot calls, where ssh
|
||||||
# supports it (not on Windows: there every command connects on its own).
|
# supports it (not on Windows: there every command connects on its own).
|
||||||
CONTROL = frame_host.control_path()
|
CONTROL = frame_host.control_path()
|
||||||
@@ -763,7 +765,7 @@ POST = {"/api/android/display": android_display, "/api/android": android,"/api/l
|
|||||||
|
|
||||||
def _pipe_reader(pipe):
|
def _pipe_reader(pipe):
|
||||||
"""Chunks from a pipe via a thread; select() can't wait on pipes on Windows."""
|
"""Chunks from a pipe via a thread; select() can't wait on pipes on Windows."""
|
||||||
chunks = queue.Queue(maxsize=64)
|
chunks = queue.Queue() # unbounded: the pump never blocks, so it ends at EOF
|
||||||
|
|
||||||
def pump():
|
def pump():
|
||||||
try:
|
try:
|
||||||
@@ -791,7 +793,8 @@ def push_file(path, dest="Downloads/"):
|
|||||||
"""Copy a file to the Frame (as scripts/push.sh): rsync where both ends have it, else scp."""
|
"""Copy a file to the Frame (as scripts/push.sh): rsync where both ends have it, else scp."""
|
||||||
name = Path(path).name
|
name = Path(path).name
|
||||||
try:
|
try:
|
||||||
if shutil.which("rsync") and ssh("command -v rsync >/dev/null && echo yes || true").strip() == "yes":
|
# Not on Windows: a Windows rsync (cwRsync, MSYS2) wouldn't take our POSIX -e quoting.
|
||||||
|
if not frame_host.WINDOWS and shutil.which("rsync") and ssh("command -v rsync >/dev/null && echo yes || true").strip() == "yes":
|
||||||
cmd = ["rsync", "-a", "-e", shlex.join(SSH), str(path), f"{FRAME}:{shlex.quote(dest)}"]
|
cmd = ["rsync", "-a", "-e", shlex.join(SSH), str(path), f"{FRAME}:{shlex.quote(dest)}"]
|
||||||
else:
|
else:
|
||||||
# Modern scp uses SFTP, so the remote path isn't parsed by a shell.
|
# Modern scp uses SFTP, so the remote path isn't parsed by a shell.
|
||||||
|
|||||||
Reference in new issue
Block a user