mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-06 18:00:43 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc40325c2d |
No files matched your search
@@ -33,7 +33,7 @@ jobs:
|
||||
archive: FramePort-windows-x64.zip
|
||||
- os: macos-latest
|
||||
archive: FramePort-macos-arm64.zip
|
||||
- os: ubuntu-latest
|
||||
- os: ubuntu-22.04 # oldest supported: the bundle needs the build host's GLib/glibc or newer
|
||||
archive: FramePort-linux-x64.tar.gz
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
@@ -367,6 +367,12 @@ Installed apps find the release themselves (self-update), so the notes are what
|
||||
powershell.exe` with `PSModulePath` removed from the environment — started under PowerShell 7 (CI's default shell,
|
||||
or a user's pwsh terminal) it couldn't run Get-AuthenticodeSignature (v0.3.0's Windows update smoke failed on it).
|
||||
`v0.3.0` is a tag without a release (that failed build); the updater shipped first in v0.3.1.
|
||||
- **Never start PowerShell with `DETACHED_PROCESS` from the packaged app**: it exits 0 without running anything (found
|
||||
in the 0.3.1→0.3.2 end-to-end test: the app quit, nothing updated). `updates.spawn_hidden` = CREATE_NEW_CONSOLE +
|
||||
hidden window; `apply()` waits until the script has logged that it runs and raises otherwise (the app then stays
|
||||
open). Windows installs of 0.3.1/0.3.2 can't update themselves: they need one manual download.
|
||||
- The Linux bundle is built on ubuntu-22.04: a 24.04-built Flutter bundle needs GLib 2.80 (`undefined symbol:
|
||||
g_once_init_enter_pointer` on 22.04).
|
||||
- **This project's GitHub identity is `spoopyghosty0`** (a dedicated account; the machine's default gh/git login is a
|
||||
different, personal account that must never touch this repo). `gh` (`~/.local/bin/gh`) uses it through
|
||||
`GH_CONFIG_DIR=~/.config/gh-spoopyghosty0` (set for Claude Code in the git-ignored `.claude/settings.local.json`);
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ runtime, the OVRPort CLI and apksigner into its data folder (Settings → Tools
|
||||
|---|---|---|
|
||||
| Windows 10/11 (x64) | `FramePort-windows-x64.zip` | `FramePort.exe` |
|
||||
| macOS (Apple Silicon) | `FramePort-macos-arm64.zip` | `FramePort.app` |
|
||||
| Linux (x64, GTK 3) | `FramePort-linux-x64.tar.gz` | `FramePort/FramePort` |
|
||||
| Linux (x64, GTK 3; Ubuntu 22.04 or newer) | `FramePort-linux-x64.tar.gz` | `FramePort/FramePort` |
|
||||
| Command line only (Python 3.11+) | `frameport-<version>-py3-none-any.whl` | `frameport --help` |
|
||||
|
||||
The command-line version installs from the wheel's release link with `uv tool install <link>` (or pipx / pip).
|
||||
|
||||
@@ -14,6 +14,7 @@ import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
@@ -46,8 +47,14 @@ def main() -> int:
|
||||
a, b = updates._signer_thumbprint(installed / "FramePort.exe"), updates._signer_thumbprint(new / "FramePort.exe")
|
||||
print(f"signer thumbprints: installed {a} new {b}")
|
||||
assert a and a == b, "signature check failed"
|
||||
script = updates.apply(new, installed, relaunch=False, pid=999999, platform=platform, wait=True)
|
||||
log = (home / "data/logs/update.log").read_text()
|
||||
# the same launch as the app (detached; apply() checks the script started), then wait for it to finish
|
||||
script = updates.apply(new, installed, relaunch=False, pid=999999, platform=platform)
|
||||
log_file = home / "data/logs/update.log"
|
||||
for _ in range(240):
|
||||
log = log_file.read_text() if log_file.exists() else ""
|
||||
if "not relaunching" in log:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
print(script.read_text()[:400], "...\n--- update.log ---\n" + log)
|
||||
installed_marker = installed / marker.relative_to(new)
|
||||
assert installed_marker.read_text() == "new", "the new files weren't installed"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# The FramePort version: the single source (pyproject reads it via hatch; CI checks a release tag matches it).
|
||||
__version__ = "0.3.2"
|
||||
__version__ = "0.3.3"
|
||||
@@ -121,8 +121,7 @@ def update_cmd(check: bool = typer.Option(False, "--check", help="Only say wheth
|
||||
if kind == "wheel" and sys.platform == "win32":
|
||||
# the running frameport.exe can't be replaced while it runs: finish the upgrade right after this exits
|
||||
line = " && ".join(subprocess.list2cmdline(c) for c in cmds)
|
||||
subprocess.Popen(f'cmd /c "timeout /t 2 /nobreak >nul && {line}"', creationflags=0x00000008 | 0x00000200,
|
||||
close_fds=True)
|
||||
updates.spawn_hidden(["cmd", "/c", f"ping -n 3 127.0.0.1 >nul && {line}"])
|
||||
typer.echo(f"Updating to FramePort {up.version} in the background: run `frameport --version` in a few seconds.")
|
||||
return
|
||||
for cmd in cmds:
|
||||
|
||||
@@ -278,6 +278,20 @@ def _powershell() -> tuple[str, dict]:
|
||||
return (str(exe) if exe.exists() else "powershell"), env
|
||||
|
||||
|
||||
START_TIMEOUT = 15 # seconds for the swap script to log that it runs
|
||||
|
||||
|
||||
def spawn_hidden(cmd: list[str], env: dict | None = None) -> subprocess.Popen:
|
||||
"""Windows: start a console program in its own hidden console, independent of FramePort. DETACHED_PROCESS
|
||||
doesn't work for PowerShell: started that way from the packaged app it exits 0 without running anything."""
|
||||
si = subprocess.STARTUPINFO()
|
||||
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
si.wShowWindow = 0 # SW_HIDE
|
||||
return subprocess.Popen(cmd, creationflags=0x00000010 | 0x00000200, # CREATE_NEW_CONSOLE, NEW_PROCESS_GROUP
|
||||
startupinfo=si, close_fds=True, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, env=env)
|
||||
|
||||
|
||||
def _signer_thumbprint(exe: Path) -> str | None:
|
||||
"""Windows: the Authenticode signer certificate's thumbprint ('' if unsigned, None if it can't be read)."""
|
||||
ps, env = _powershell()
|
||||
@@ -449,16 +463,26 @@ def apply(app: Path, target: Path | None = None, relaunch: bool = True, pid: int
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
script = updates_dir() / ("apply.ps1" if platform == "win32" else "apply.sh")
|
||||
script.write_text(swap_script(app, target, pid, platform, relaunch, log), encoding="utf-8")
|
||||
started = log.stat().st_size if log.exists() else 0
|
||||
if platform == "win32":
|
||||
ps, env = _powershell()
|
||||
cmd = [ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File",
|
||||
str(script)]
|
||||
flags = 0 if wait else (0x00000008 | 0x00000200 | 0x08000000) # DETACHED, NEW_PROCESS_GROUP, NO_WINDOW
|
||||
proc = subprocess.Popen(cmd, creationflags=flags, close_fds=True, stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env)
|
||||
proc = spawn_hidden([ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden",
|
||||
"-File", str(script)], env=env)
|
||||
else:
|
||||
proc = subprocess.Popen(["/bin/sh", str(script)], start_new_session=not wait, close_fds=True,
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
if not wait: # make sure the script really runs before FramePort quits (it logs first thing)
|
||||
deadline = time.time() + START_TIMEOUT
|
||||
while time.time() < deadline:
|
||||
if log.exists() and f"(pid {pid})" in log.read_text(errors="replace")[started:]:
|
||||
break
|
||||
if proc.poll() not in (None, 0):
|
||||
break
|
||||
time.sleep(0.2)
|
||||
else:
|
||||
raise UpdateError(f"the update script didn't start (see {log})")
|
||||
if proc.poll() not in (None, 0):
|
||||
raise UpdateError(f"the update script failed to start (exit {proc.returncode})")
|
||||
if wait:
|
||||
proc.wait()
|
||||
return script
|
||||
@@ -213,3 +213,33 @@ def test_cli_hint_once_a_day_without_waiting(monkeypatch):
|
||||
monkeypatch.setenv("FRAMEPORT_NO_UPDATE_CHECK", "1")
|
||||
library.set_setting("update.cli_hint", None)
|
||||
assert "is available" not in runner.invoke(app, ["list"]).output
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX swap script")
|
||||
def test_apply_detached_checks_the_script_started(monkeypatch, tmp_path):
|
||||
import time
|
||||
|
||||
up = updates.update_from_release(release(), "FramePort-linux-x64.tar.gz")
|
||||
_fake_downloads(monkeypatch, {"FramePort-linux-x64.tar.gz": _linux_archive("9.9.9")})
|
||||
app = updates.prepare(up, platform="linux")
|
||||
installed = tmp_path / "apps/FramePort"
|
||||
installed.mkdir(parents=True)
|
||||
(installed / "FramePort").write_text("old")
|
||||
updates.apply(app, installed, relaunch=False, pid=2 ** 22 + 2, platform="linux") # returns once the script runs
|
||||
log = updates.user_data_dir() / "logs/update.log"
|
||||
for _ in range(100):
|
||||
if "not relaunching" in log.read_text():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
assert (installed / "data/version.txt").read_text() == "9.9.9"
|
||||
|
||||
|
||||
def test_apply_reports_a_script_that_never_starts(monkeypatch, tmp_path):
|
||||
installed = tmp_path / "FramePort"
|
||||
installed.mkdir()
|
||||
staged = tmp_path / "staged"
|
||||
staged.mkdir()
|
||||
monkeypatch.setattr(updates, "START_TIMEOUT", 0.5)
|
||||
monkeypatch.setattr(updates.subprocess, "Popen", lambda *a, **k: type("P", (), {"poll": lambda self: None})())
|
||||
with pytest.raises(updates.UpdateError):
|
||||
updates.apply(staged, installed, relaunch=False, pid=1, platform="linux")
|
||||
Reference in new issue
Block a user