Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
564e051d1a |
No files matched your search
@@ -17,6 +17,23 @@ allowed to change behaviour, not just add to it.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.6] — 2026-09-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Accepting "reopen in Windows Terminal" killed the session** in the downloadable
|
||||
builds. A new window opened, printed `Security validation failure: parent process has
|
||||
different executable!`, and exited — while the window that made the offer had already
|
||||
closed. Worse than never offering.
|
||||
|
||||
A one-file build unpacks itself and re-runs itself, the two halves coordinating through
|
||||
private environment variables. Those were being handed to the new copy, which concluded
|
||||
it was the second half of a launch it never made, checked that its parent was the same
|
||||
program, found Windows Terminal instead, and stopped. They are stripped now.
|
||||
|
||||
It worked perfectly when run from source, which is the whole lesson: the packaged build
|
||||
is its own platform.
|
||||
|
||||
## [0.2.5] — 2026-09-07
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -68,14 +68,14 @@ if anything goes wrong at startup the window closes before you can read why.
|
||||
**Windows** — open Windows Terminal or PowerShell, `cd` to your downloads, then:
|
||||
|
||||
```powershell
|
||||
.\meshterm-0.2.5-windows-x64.exe
|
||||
.\meshterm-0.2.6-windows-x64.exe
|
||||
```
|
||||
|
||||
**macOS and Linux** — open Terminal, `cd` to your downloads, then:
|
||||
|
||||
```bash
|
||||
chmod +x meshterm-0.2.5-*
|
||||
./meshterm-0.2.5-*
|
||||
chmod +x meshterm-0.2.6-*
|
||||
./meshterm-0.2.6-*
|
||||
```
|
||||
|
||||
### A word about terminals, on Windows
|
||||
@@ -114,7 +114,7 @@ becomes just `meshterm` from anywhere.
|
||||
> project.
|
||||
>
|
||||
> - **macOS**: *"cannot be opened because the developer cannot be verified"*. Clear it with
|
||||
> `xattr -d com.apple.quarantine meshterm-0.2.5-*` and run it again.
|
||||
> `xattr -d com.apple.quarantine meshterm-0.2.6-*` and run it again.
|
||||
> - **Windows**: *"Windows protected your PC"*. Click **More info → Run anyway**.
|
||||
>
|
||||
> Every release ships a `SHA256SUMS` file if you'd rather check the download first.
|
||||
@@ -124,10 +124,10 @@ becomes just `meshterm` from anywhere.
|
||||
> MeshTerm uses that same folder. Set `MESHTERM_HOME` to try one in isolation:
|
||||
>
|
||||
> ```bash
|
||||
> MESHTERM_HOME=~/meshterm-test ./meshterm-0.2.5-* # macOS, Linux
|
||||
> MESHTERM_HOME=~/meshterm-test ./meshterm-0.2.6-* # macOS, Linux
|
||||
> ```
|
||||
> ```powershell
|
||||
> $env:MESHTERM_HOME = "$HOME\meshterm-test"; .\meshterm-0.2.5-windows-x64.exe
|
||||
> $env:MESHTERM_HOME = "$HOME\meshterm-test"; .\meshterm-0.2.6-windows-x64.exe
|
||||
> ```
|
||||
>
|
||||
> Same variable if you run two radios and want them kept apart.
|
||||
|
||||
@@ -5,7 +5,7 @@ Supports USB, Bluetooth and TCP companion connection. For Windows, macOS and Lin
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
__version__ = "0.2.5"
|
||||
__version__ = "0.2.6"
|
||||
__author__ = "Jean-Pierre Martineau"
|
||||
|
||||
#: The year MeshTerm was first published; the copyright span starts here.
|
||||
|
||||
@@ -61,6 +61,45 @@ def own_command() -> list[str]:
|
||||
return [sys.executable, "-m", "meshterm", *sys.argv[1:]]
|
||||
|
||||
|
||||
def child_environment() -> dict[str, str]:
|
||||
"""The environment for a fresh launch of MeshTerm, minus this bundle's bookkeeping.
|
||||
|
||||
A PyInstaller one-file build runs in two stages: the executable unpacks itself into a
|
||||
temporary directory and then runs itself again as a child, the two halves coordinating
|
||||
through ``_PYI*`` environment variables. Those must not reach a *new* launch of the
|
||||
executable. The bootloader would find them, conclude it is the second stage of a launch
|
||||
it never made, check that its parent is the same program — find Windows Terminal
|
||||
instead — and abort::
|
||||
|
||||
[PYI-33368:ERROR] Security validation failure: parent process has different
|
||||
executable!
|
||||
|
||||
Which is the whole session gone, in a brand new window holding a message nobody can act
|
||||
on, while the window that offered the move has already closed. Removing them makes the
|
||||
new process a first stage, which is what it actually is.
|
||||
|
||||
Returns:
|
||||
The environment to start the new process with.
|
||||
"""
|
||||
environment = dict(os.environ)
|
||||
environment[REOPENED_ENV] = "1"
|
||||
if not getattr(sys, "frozen", False):
|
||||
return environment
|
||||
|
||||
for key in [key for key in environment if key.startswith("_PYI")]:
|
||||
del environment[key]
|
||||
environment.pop("_MEIPASS2", None) # the name older bootloaders used
|
||||
# The bootloader points the loader at the unpacked bundle and parks the caller's own
|
||||
# value beside it. The new process unpacks its own copy, so it wants the original.
|
||||
for name in ("LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH"):
|
||||
original = environment.pop(f"{name}_ORIG", None)
|
||||
if original is not None:
|
||||
environment[name] = original
|
||||
else:
|
||||
environment.pop(name, None)
|
||||
return environment
|
||||
|
||||
|
||||
def reopen() -> bool:
|
||||
"""Start this session again in Windows Terminal, in the same directory.
|
||||
|
||||
@@ -74,7 +113,6 @@ def reopen() -> bool:
|
||||
terminal = available()
|
||||
if terminal is None:
|
||||
return False
|
||||
environment = dict(os.environ, **{REOPENED_ENV: "1"})
|
||||
try:
|
||||
# `--` ends wt's own option parsing, so MeshTerm's flags reach MeshTerm rather
|
||||
# than being read as Windows Terminal's (verified: `--mock` and `--profile x`
|
||||
@@ -82,7 +120,7 @@ def reopen() -> bool:
|
||||
# on the command line depend on.
|
||||
subprocess.Popen( # noqa: S603 - the argv is ours, not the reader's
|
||||
[terminal, "-d", os.getcwd(), "--", *own_command()],
|
||||
env=environment,
|
||||
env=child_environment(),
|
||||
close_fds=True,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -73,3 +73,61 @@ def test_the_program_name_is_never_passed_on_as_an_argument(
|
||||
monkeypatch.setattr(sys, "executable", r"C:\meshterm.exe")
|
||||
monkeypatch.setattr(sys, "argv", [r"C:\some\other\name.exe"])
|
||||
assert winterminal.own_command() == [r"C:\meshterm.exe"]
|
||||
|
||||
|
||||
def test_the_bundles_own_bookkeeping_never_reaches_the_new_process(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The bug the frozen build found, and the only one the source build could not.
|
||||
|
||||
A PyInstaller one-file executable unpacks itself and re-runs itself, the two halves
|
||||
talking through ``_PYI*`` variables. Passing those on made the reopened MeshTerm think
|
||||
it was the second half of a launch it never made; it checked its parent, found Windows
|
||||
Terminal, and killed itself with a bootloader error in a window that had just opened.
|
||||
"""
|
||||
monkeypatch.setattr(sys, "frozen", True, raising=False)
|
||||
monkeypatch.setenv("_PYI_ARCHIVE_FILE", r"C:\Downloads\meshterm.exe")
|
||||
monkeypatch.setenv("_PYI_PARENT_PROCESS_LEVEL", "0")
|
||||
monkeypatch.setenv("_MEIPASS2", r"C:\Temp\_MEI123")
|
||||
monkeypatch.setenv("MESHTERM_HOME", r"C:\mine")
|
||||
|
||||
environment = winterminal.child_environment()
|
||||
|
||||
assert not [key for key in environment if key.startswith("_PYI")]
|
||||
assert "_MEIPASS2" not in environment
|
||||
assert environment["MESHTERM_HOME"] == r"C:\mine" # the reader's own settings survive
|
||||
assert environment[winterminal.REOPENED_ENV] == "1"
|
||||
|
||||
|
||||
def test_a_source_run_keeps_the_environment_it_was_given(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Nothing is stripped when there is no bundle, so a dev run is passed through whole.
|
||||
|
||||
``LD_LIBRARY_PATH`` is the one to watch: unfrozen it is the reader's, and dropping it
|
||||
would change how the new process loads its libraries for no reason at all.
|
||||
"""
|
||||
monkeypatch.delattr(sys, "frozen", raising=False)
|
||||
monkeypatch.setenv("LD_LIBRARY_PATH", "/opt/mine/lib")
|
||||
assert winterminal.child_environment()["LD_LIBRARY_PATH"] == "/opt/mine/lib"
|
||||
|
||||
|
||||
def test_the_loader_path_the_bootloader_replaced_is_put_back(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The bootloader points the loader at its own unpacked copy and parks the original.
|
||||
|
||||
The new process unpacks a bundle of its own, so it wants the value the reader started
|
||||
with — and where there was none, none.
|
||||
"""
|
||||
monkeypatch.setattr(sys, "frozen", True, raising=False)
|
||||
monkeypatch.setenv("LD_LIBRARY_PATH", "/tmp/_MEI999")
|
||||
monkeypatch.setenv("LD_LIBRARY_PATH_ORIG", "/opt/mine/lib")
|
||||
monkeypatch.setenv("DYLD_LIBRARY_PATH", "/tmp/_MEI999")
|
||||
monkeypatch.delenv("DYLD_LIBRARY_PATH_ORIG", raising=False)
|
||||
|
||||
environment = winterminal.child_environment()
|
||||
|
||||
assert environment["LD_LIBRARY_PATH"] == "/opt/mine/lib"
|
||||
assert "LD_LIBRARY_PATH_ORIG" not in environment
|
||||
assert "DYLD_LIBRARY_PATH" not in environment
|
||||
Reference in new issue
Block a user