invisible-firefox 0.2.0__tar.gz

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.
Files changed (30) hide show
  1. invisible_firefox-0.2.0/.gitignore +7 -0
  2. invisible_firefox-0.2.0/LICENSE +21 -0
  3. invisible_firefox-0.2.0/PKG-INFO +91 -0
  4. invisible_firefox-0.2.0/README.md +56 -0
  5. invisible_firefox-0.2.0/pyproject.toml +94 -0
  6. invisible_firefox-0.2.0/src/invisible_firefox/__init__.py +88 -0
  7. invisible_firefox-0.2.0/src/invisible_firefox/__main__.py +4 -0
  8. invisible_firefox-0.2.0/src/invisible_firefox/manager/__init__.py +0 -0
  9. invisible_firefox-0.2.0/src/invisible_firefox/manager/fingerprint.py +36 -0
  10. invisible_firefox-0.2.0/src/invisible_firefox/manager/launcher.py +216 -0
  11. invisible_firefox-0.2.0/src/invisible_firefox/manager/models.py +52 -0
  12. invisible_firefox-0.2.0/src/invisible_firefox/manager/paths.py +29 -0
  13. invisible_firefox-0.2.0/src/invisible_firefox/manager/settings.py +33 -0
  14. invisible_firefox-0.2.0/src/invisible_firefox/manager/store.py +89 -0
  15. invisible_firefox-0.2.0/src/invisible_firefox/manager/sx.py +157 -0
  16. invisible_firefox-0.2.0/src/invisible_firefox/py.typed +0 -0
  17. invisible_firefox-0.2.0/src/invisible_firefox/ui/__init__.py +0 -0
  18. invisible_firefox-0.2.0/src/invisible_firefox/ui/app.py +7 -0
  19. invisible_firefox-0.2.0/src/invisible_firefox/ui/web/index.html +690 -0
  20. invisible_firefox-0.2.0/src/invisible_firefox/ui/web_app.py +346 -0
  21. invisible_firefox-0.2.0/tests/conftest.py +2 -0
  22. invisible_firefox-0.2.0/tests/test_fingerprint.py +24 -0
  23. invisible_firefox-0.2.0/tests/test_launcher.py +103 -0
  24. invisible_firefox-0.2.0/tests/test_models.py +18 -0
  25. invisible_firefox-0.2.0/tests/test_paths.py +11 -0
  26. invisible_firefox-0.2.0/tests/test_seal_floor.py +371 -0
  27. invisible_firefox-0.2.0/tests/test_status.py +48 -0
  28. invisible_firefox-0.2.0/tests/test_store.py +59 -0
  29. invisible_firefox-0.2.0/tests/test_sx.py +110 -0
  30. invisible_firefox-0.2.0/tests/test_web_api.py +69 -0
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ dist/
6
+ build/
7
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Federico Elia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: invisible-firefox
3
+ Version: 0.2.0
4
+ Summary: Antidetect browser profile manager for the patched Firefox (manual use).
5
+ Project-URL: Homepage, https://github.com/feder-cr/invisible_firefox
6
+ Project-URL: Repository, https://github.com/feder-cr/invisible_firefox
7
+ Project-URL: Issues, https://github.com/feder-cr/invisible_firefox/issues
8
+ Author-email: feder-cr <85809106+feder-cr@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: anti-detect,browser,fingerprint,firefox,profile-manager
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
23
+ Classifier: Topic :: Utilities
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: invisible-core==18.0.0
27
+ Requires-Dist: platformdirs>=4
28
+ Requires-Dist: psutil>=5.9
29
+ Requires-Dist: pywebview>=4.4; sys_platform != 'linux'
30
+ Requires-Dist: pywebview[qt]>=4.4; sys_platform == 'linux'
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1; extra == 'dev'
33
+ Requires-Dist: pytest>=7; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ <p>
37
+ <a href="https://github.com/feder-cr/invisible_firefox/blob/main/LICENSE"><img src="https://raw.githubusercontent.com/feder-cr/invisible_firefox/main/docs/badges/license.svg" alt="License: MIT"></a>
38
+ <a href="https://www.python.org/downloads/"><img src="https://raw.githubusercontent.com/feder-cr/invisible_firefox/main/docs/badges/python.svg" alt="Python 3.11+"></a>
39
+ <a href="https://github.com/feder-cr/firefox_antidetect_patch/releases"><img src="https://raw.githubusercontent.com/feder-cr/invisible_firefox/main/docs/badges/firefox.svg" alt="Firefox 151.0"></a>
40
+ </p>
41
+
42
+ <h3 align="center">Run many undetectable browser profiles on one patched Firefox.</h3>
43
+
44
+ One desktop app, many identities. Each profile is a distinct fingerprint (GPU, canvas, fonts, audio, WebRTC) set at the C++ source level plus its own proxy and saved logins, so every profile passes bot detection on its own. Hit Launch and a real Firefox window opens with that identity.
45
+
46
+ <div align="center">
47
+ <img src="https://raw.githubusercontent.com/feder-cr/invisible_firefox/6485164a1bb54e85e6c215d020501dc3a0f90f47/docs/screenshot.png" alt="invisible_firefox" width="820">
48
+ </div>
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install invisible-firefox
54
+ python -m invisible_firefox
55
+ ```
56
+
57
+ Everything it needs is downloaded automatically, nothing to fetch by hand: the patched Firefox binary (once, SHA256-verified, cached) the moment the app opens, and the GeoIP database when a proxy needs it. Runs on **Windows**, **Linux** and **macOS**.
58
+
59
+ Built on [invisible_core](https://github.com/feder-cr/invisible_core); it launches the binary directly, no Playwright.
60
+
61
+ ## What you get
62
+
63
+ - **One identity per profile** - GPU, screen, fonts and ~400 other fields from a seed, stable across launches.
64
+ - **Persistent** - each profile keeps its own cookies, storage and logins in its own directory.
65
+ - **Per-profile proxy** - none, or [sx.org](https://sx.org/?c=invisible_playwright) residential, with country and city per profile.
66
+ - **Live status** - see which profiles are running; close the browser and the list updates itself.
67
+
68
+ ## Proxies
69
+
70
+ Around 90% of proxies are public, so their IPs are already known and blocked. For the clean 10%, residential IPs that aren't already known, we recommend [sx.org](https://sx.org/?c=invisible_playwright): set your API key once in the Proxies menu, then pick a country and city per profile. New accounts get 3 GB free with code `STEALTH`.
71
+
72
+ ## Related projects
73
+
74
+ - **[invisible_core](https://github.com/feder-cr/invisible_core)** - the pure-config engine (seed to fingerprint to prefs, binary download, proxy, geo) this app runs on.
75
+ - **[invisible_playwright](https://github.com/feder-cr/invisible_playwright)** - the same patched Firefox driven by Playwright, for automation.
76
+ - **[firefox_antidetect_patch](https://github.com/feder-cr/firefox_antidetect_patch)** - the C++ patches against Firefox that produce the binary.
77
+
78
+ ## License
79
+
80
+ MIT - see [LICENSE](https://github.com/feder-cr/invisible_firefox/blob/main/LICENSE). The patched Firefox binary is distributed under the MPL-2.0 (Firefox upstream license).
81
+
82
+ ## Disclaimer
83
+
84
+ This project is for educational purposes only. It is provided as-is, with no warranties. I take no responsibility for how it is used. Use it at your own risk and in compliance with the laws of your jurisdiction.
85
+
86
+ ---
87
+
88
+ <p align="center">
89
+ Built by <a href="https://it.linkedin.com/in/federico-elia-5199951b6">Federico Elia</a>
90
+ &nbsp;<a href="https://it.linkedin.com/in/federico-elia-5199951b6"><img src="https://raw.githubusercontent.com/feder-cr/invisible_firefox/main/docs/badges/linkedin.svg" alt="LinkedIn"></a>
91
+ </p>
@@ -0,0 +1,56 @@
1
+ <p>
2
+ <a href="https://github.com/feder-cr/invisible_firefox/blob/main/LICENSE"><img src="https://raw.githubusercontent.com/feder-cr/invisible_firefox/main/docs/badges/license.svg" alt="License: MIT"></a>
3
+ <a href="https://www.python.org/downloads/"><img src="https://raw.githubusercontent.com/feder-cr/invisible_firefox/main/docs/badges/python.svg" alt="Python 3.11+"></a>
4
+ <a href="https://github.com/feder-cr/firefox_antidetect_patch/releases"><img src="https://raw.githubusercontent.com/feder-cr/invisible_firefox/main/docs/badges/firefox.svg" alt="Firefox 151.0"></a>
5
+ </p>
6
+
7
+ <h3 align="center">Run many undetectable browser profiles on one patched Firefox.</h3>
8
+
9
+ One desktop app, many identities. Each profile is a distinct fingerprint (GPU, canvas, fonts, audio, WebRTC) set at the C++ source level plus its own proxy and saved logins, so every profile passes bot detection on its own. Hit Launch and a real Firefox window opens with that identity.
10
+
11
+ <div align="center">
12
+ <img src="https://raw.githubusercontent.com/feder-cr/invisible_firefox/6485164a1bb54e85e6c215d020501dc3a0f90f47/docs/screenshot.png" alt="invisible_firefox" width="820">
13
+ </div>
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install invisible-firefox
19
+ python -m invisible_firefox
20
+ ```
21
+
22
+ Everything it needs is downloaded automatically, nothing to fetch by hand: the patched Firefox binary (once, SHA256-verified, cached) the moment the app opens, and the GeoIP database when a proxy needs it. Runs on **Windows**, **Linux** and **macOS**.
23
+
24
+ Built on [invisible_core](https://github.com/feder-cr/invisible_core); it launches the binary directly, no Playwright.
25
+
26
+ ## What you get
27
+
28
+ - **One identity per profile** - GPU, screen, fonts and ~400 other fields from a seed, stable across launches.
29
+ - **Persistent** - each profile keeps its own cookies, storage and logins in its own directory.
30
+ - **Per-profile proxy** - none, or [sx.org](https://sx.org/?c=invisible_playwright) residential, with country and city per profile.
31
+ - **Live status** - see which profiles are running; close the browser and the list updates itself.
32
+
33
+ ## Proxies
34
+
35
+ Around 90% of proxies are public, so their IPs are already known and blocked. For the clean 10%, residential IPs that aren't already known, we recommend [sx.org](https://sx.org/?c=invisible_playwright): set your API key once in the Proxies menu, then pick a country and city per profile. New accounts get 3 GB free with code `STEALTH`.
36
+
37
+ ## Related projects
38
+
39
+ - **[invisible_core](https://github.com/feder-cr/invisible_core)** - the pure-config engine (seed to fingerprint to prefs, binary download, proxy, geo) this app runs on.
40
+ - **[invisible_playwright](https://github.com/feder-cr/invisible_playwright)** - the same patched Firefox driven by Playwright, for automation.
41
+ - **[firefox_antidetect_patch](https://github.com/feder-cr/firefox_antidetect_patch)** - the C++ patches against Firefox that produce the binary.
42
+
43
+ ## License
44
+
45
+ MIT - see [LICENSE](https://github.com/feder-cr/invisible_firefox/blob/main/LICENSE). The patched Firefox binary is distributed under the MPL-2.0 (Firefox upstream license).
46
+
47
+ ## Disclaimer
48
+
49
+ This project is for educational purposes only. It is provided as-is, with no warranties. I take no responsibility for how it is used. Use it at your own risk and in compliance with the laws of your jurisdiction.
50
+
51
+ ---
52
+
53
+ <p align="center">
54
+ Built by <a href="https://it.linkedin.com/in/federico-elia-5199951b6">Federico Elia</a>
55
+ &nbsp;<a href="https://it.linkedin.com/in/federico-elia-5199951b6"><img src="https://raw.githubusercontent.com/feder-cr/invisible_firefox/main/docs/badges/linkedin.svg" alt="LinkedIn"></a>
56
+ </p>
@@ -0,0 +1,94 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "invisible-firefox"
7
+ version = "0.2.0"
8
+ description = "Antidetect browser profile manager for the patched Firefox (manual use)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "feder-cr", email = "85809106+feder-cr@users.noreply.github.com" }]
13
+ keywords = ["firefox", "browser", "profile-manager", "anti-detect", "fingerprint"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Web Environment",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: Microsoft :: Windows",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Operating System :: MacOS",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
26
+ "Topic :: Utilities",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = [
30
+ # An exact version, resolved from the index. It is the ONLY place the expected
31
+ # core version is written: the import-time check in invisible_firefox/__init__.py
32
+ # reads this requirement back with importlib.metadata.requires(), so there is no
33
+ # second copy to drift.
34
+ #
35
+ # Why exact and not a bare direct reference (which is what this was until the
36
+ # move to the index): a direct reference carries no version specifier, so there
37
+ # is nothing for pip to compare - `pip check` returns "No broken requirements
38
+ # found", exit 0, on a definitively broken environment, and direct_url.json goes
39
+ # on recording the old commit. The same mismatch written as a specifier is caught
40
+ # with exit 1, and a plain `pip install invisible-firefox` repairs a broken env
41
+ # because pip still follows Requires-Dist.
42
+ #
43
+ # The cost, accepted deliberately: a core-only release (CORE_REVISION bump) now
44
+ # needs this literal moved and a manager release. The residue an exact pin still
45
+ # cannot see (--no-deps, lockfile installs, a hand-patched site-packages) is what
46
+ # the import-time check in __init__.py is for.
47
+ "invisible-core==18.0.0",
48
+ "platformdirs>=4",
49
+ # cross-platform process tree: on Windows firefox.exe is a launcher stub
50
+ # that spawns the browser as a child and exits, so we track the real
51
+ # browser process (found by profile dir) via psutil - same code Win/Linux/mac.
52
+ "psutil>=5.9",
53
+ # pywebview backend per OS: Windows -> WebView2, macOS -> WKWebView (native,
54
+ # no extra deps); Linux needs an explicit webview backend, so pull the Qt one
55
+ # (pip-installable, unlike the GTK/WebKit2GTK system packages).
56
+ "pywebview>=4.4; sys_platform != 'linux'",
57
+ "pywebview[qt]>=4.4; sys_platform == 'linux'",
58
+ ]
59
+
60
+ [project.optional-dependencies]
61
+ dev = ["pytest>=7", "build>=1"]
62
+
63
+ # GUI app: gui-scripts so launching it does not open a console window on Windows.
64
+ # `python -m invisible_firefox` stays the documented path and is the one to use
65
+ # when something fails at import, because it keeps stderr attached.
66
+ [project.gui-scripts]
67
+ invisible-firefox = "invisible_firefox.ui.app:run"
68
+
69
+ [project.urls]
70
+ Homepage = "https://github.com/feder-cr/invisible_firefox"
71
+ Repository = "https://github.com/feder-cr/invisible_firefox"
72
+ Issues = "https://github.com/feder-cr/invisible_firefox/issues"
73
+
74
+ # No [tool.hatch.metadata] allow-direct-references: with the opt-in absent,
75
+ # hatchling refuses to build a direct reference back into the metadata, so the
76
+ # `invisible-core==` pin above cannot silently regress to a git URL - PyPI would
77
+ # reject the upload anyway, but this fails at build time instead.
78
+
79
+ [tool.hatch.build.targets.wheel]
80
+ packages = ["src/invisible_firefox"]
81
+
82
+ # Without an explicit include list hatchling falls back to "everything the VCS
83
+ # does not ignore", which shipped .githooks/pre-push, the whole docs/ folder (a
84
+ # 96 KB screenshot plus four badge SVGs the README loads from raw.githubusercontent,
85
+ # not from the archive) and packaging/ (the PyInstaller recipe for the standalone
86
+ # bundle - a build input, not a runtime input): 38 members, now 30. Same shape as
87
+ # the other two packages: source, tests, README, LICENSE, pyproject. .gitignore
88
+ # stays either way - hatchling puts it in every sdist, include list or not.
89
+ [tool.hatch.build.targets.sdist]
90
+ include = ["src/invisible_firefox", "tests", "README.md", "LICENSE", "pyproject.toml"]
91
+
92
+ [tool.pytest.ini_options]
93
+ addopts = "-m 'not integration'"
94
+ markers = ["integration: launches the real patched Firefox binary named by the seal"]
@@ -0,0 +1,88 @@
1
+ """invisible_firefox - profile manager for the patched Firefox.
2
+
3
+ The check below runs before anything else in this package is imported. It
4
+ compares the invisible-core that will actually execute against the
5
+ `invisible-core==` pin this distribution declares in its own metadata, so a
6
+ core from another engine release is never quietly used to launch a browser
7
+ whose configuration nobody matched.
8
+
9
+ A mismatch is REPAIRED, not just reported: invisible_core._pin installs the
10
+ declared core and picks it up in this process, so the script that imported us
11
+ keeps running. That is only sound at this exact point, while invisible_core has
12
+ not been imported by anybody yet, so the snapshot below is taken BEFORE the
13
+ import and passed in. If it says the core was already loaded, the repair refuses
14
+ itself and the message is printed instead. Every other failure of the repair
15
+ (no network, a read-only site-packages, no pip) degrades to that same message.
16
+
17
+ The comparison, the repair and the requirement parser all live in
18
+ invisible_core._pin, shared with the other consumer, and the expected version is
19
+ read back from this package's Requires-Dist. Nothing here holds a version
20
+ literal.
21
+
22
+ WHAT IS LOCAL, AND WHY. A module inside invisible_core cannot report that
23
+ invisible_core is absent, unimportable, or too damaged to derive its own
24
+ version: by the time it runs, it imported. So the try/except below is the only
25
+ part of the check this package owns, and it classifies the failure three ways:
26
+
27
+ * an ImportError naming a module under invisible_core - the core is missing or
28
+ too old to carry the check;
29
+ * an ImportError naming anything else - the core is there but one of ITS
30
+ dependencies is not, which is what `pip install --no-deps` leaves behind;
31
+ * anything that is not an ImportError - the core is on disk and partially
32
+ destroyed. Measured: delete invisible_core/seal.json and the import dies
33
+ with a raw FileNotFoundError naming an absolute path, because
34
+ invisible_core.__init__ reaches _version.py, which reads the seal.
35
+
36
+ All three remedies are `pip install --force-reinstall invisible-firefox`, so pip
37
+ resolves the right core version out of our own Requires-Dist and no version
38
+ literal appears here. None of the three is auto-repaired: the version to install
39
+ can only come from the requirement parser, which is inside the core that just
40
+ failed to import, and the only remedy that needs no version literal is a
41
+ force-reinstall of the distribution that is halfway through importing right now.
42
+
43
+ This package has no Playwright and no protocol channel to the browser, so this
44
+ is its only cross-check. Keep it that way: _pin must stay import-cheap.
45
+ """
46
+
47
+ import sys as _sys
48
+
49
+ # Captured BEFORE the core is imported. It is the whole soundness argument for
50
+ # the in-process repair: if nothing has imported invisible_core yet, nobody is
51
+ # holding an object from the version the repair is about to replace. One line
52
+ # later this is always True, so it has to be read here.
53
+ _CORE_PREIMPORTED = any(
54
+ _name == "invisible_core" or _name.startswith("invisible_core.")
55
+ for _name in list(_sys.modules)
56
+ )
57
+
58
+ try:
59
+ from invisible_core._pin import enforce_core_pin as _enforce_core_pin
60
+ except ImportError as _e: # pragma: no cover - exercised by tests/test_seal_floor.py
61
+ _missing = getattr(_e, "name", "") or ""
62
+ if _missing and not _missing.startswith("invisible_core"):
63
+ raise ImportError(
64
+ f"invisible-core is installed but cannot be imported: no module named "
65
+ f"{_missing!r}.\n"
66
+ f"That is one of its own dependencies, so it was installed without "
67
+ f"dependency resolution (pip install --no-deps, or a lockfile that "
68
+ f"lists the packages separately).\n"
69
+ f"Run: pip install --force-reinstall invisible-firefox"
70
+ ) from _e
71
+ raise ImportError(
72
+ "invisible-core is missing, or too old for this invisible-firefox: it does "
73
+ "not carry the release-pin check (invisible_core._pin), so the engine it "
74
+ "downloads cannot be matched against the configuration this package ships.\n"
75
+ "Run: pip install --force-reinstall invisible-firefox"
76
+ ) from _e
77
+ except Exception as _e: # pragma: no cover - exercised by tests/test_seal_floor.py
78
+ raise ImportError(
79
+ f"invisible-core is installed but it cannot even determine its own version: "
80
+ f"importing it raised {type(_e).__name__}: {_e}\n"
81
+ f"That is a partial or damaged installation, not an old one. A missing or "
82
+ f"unreadable invisible_core/seal.json is the usual cause, and it is read "
83
+ f"while invisible_core is still importing, so nothing inside the core can "
84
+ f"report it.\n"
85
+ f"Run: pip install --force-reinstall invisible-firefox"
86
+ ) from _e
87
+
88
+ _enforce_core_pin("invisible-firefox", core_preimported=_CORE_PREIMPORTED)
@@ -0,0 +1,4 @@
1
+ from .ui.app import run
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(run())
@@ -0,0 +1,36 @@
1
+ """Display-ready fingerprint summary for the preview panel."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from typing import Any, Dict, Optional
6
+
7
+ from invisible_core import generate_profile
8
+
9
+
10
+ def _clean_gpu(renderer: str, vendor: str) -> str:
11
+ """Turn a raw WebGL renderer string into a short, human-readable GPU name.
12
+
13
+ Real renderers are ugly ANGLE strings, e.g.
14
+ ``ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 Laptop GPU (0x00002D58) Direct3D11 vs_5_0 ps_5_0, D3D11)``
15
+ -> ``NVIDIA GeForce RTX 4060 Laptop GPU``.
16
+ """
17
+ r = (renderer or vendor or "").strip()
18
+ if r.upper().startswith("ANGLE") and "(" in r:
19
+ inner = r[r.find("(") + 1: r.rfind(")")] if ")" in r else r[r.find("(") + 1:]
20
+ parts = inner.split(",")
21
+ r = (parts[1] if len(parts) > 1 else parts[0]).strip()
22
+ r = re.sub(r"\s*\(0x[0-9a-fA-F]+\).*$", "", r) # drop "(0x2D58) Direct3D11 ..."
23
+ r = re.sub(r"\s+Direct3D.*$", "", r)
24
+ r = re.sub(r"\s+vs_\d.*$", "", r).strip()
25
+ return r or "-"
26
+
27
+
28
+ def fingerprint_summary(seed: int, pin: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
29
+ p = generate_profile(seed=seed, pin=pin)
30
+ return {
31
+ "gpu": _clean_gpu(p.gpu.renderer, p.gpu.vendor),
32
+ "gpu_vendor": p.gpu.vendor,
33
+ "gpu_renderer": p.gpu.renderer,
34
+ "screen": f"{p.screen.width}x{p.screen.height}",
35
+ "hardware_concurrency": p.hardware.concurrency,
36
+ }
@@ -0,0 +1,216 @@
1
+ """Direct-launch orchestration: Profile -> running patched Firefox subprocess."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ import time
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Dict, List, Optional
9
+
10
+ try:
11
+ import psutil # cross-platform process tree (Win/Linux/mac)
12
+ except Exception: # pragma: no cover - psutil is a declared dependency
13
+ psutil = None # type: ignore
14
+
15
+ from invisible_core import build_launch_plan as _core_build_launch_plan, LaunchPlan
16
+
17
+ from .models import Profile
18
+ from . import paths
19
+ from . import settings as _settings
20
+ from . import sx as _sx
21
+
22
+
23
+ def resolve_launch_proxy(proxy: Optional[Dict[str, Any]], base: Optional[Path] = None):
24
+ """A profile's ``proxy`` is an INTENT. Turn it into a concrete proxy dict:
25
+ ``None`` -> ``None``; an SX intent (``{"provider":"sx",...}``) -> a live
26
+ SOCKS5 endpoint via the SX API; an already-concrete ``{"server":...}`` dict
27
+ -> itself."""
28
+ if not proxy:
29
+ return None
30
+ if proxy.get("provider") == "sx":
31
+ return _sx.resolve_proxy(proxy, _settings.load_settings(base))
32
+ return proxy
33
+
34
+
35
+ @dataclass
36
+ class LaunchHandle:
37
+ pid: int # the REAL browser pid (resolved), NOT the launcher stub on Windows
38
+ profile_id: str
39
+ process: Any # the subprocess.Popen we spawned (the launcher stub on Windows)
40
+ browser: Any = None # psutil.Process of the real browser, when resolved (else None)
41
+
42
+
43
+ def build_launch_plan(profile: Profile, base: Optional[Path] = None) -> LaunchPlan:
44
+ """Resolve the profile's proxy intent (SX -> concrete endpoint) and its
45
+ profile dir, then delegate the whole session setup to
46
+ ``invisible_core.build_launch_plan``. The manager owns only SX resolution and
47
+ where the profile lives; the fingerprint, geo, prefs, user.js, env and argv
48
+ are all invisible_core's job (one shared code path with the wrapper).
49
+
50
+ The manager does not choose a Firefox at all. It names no tag and no path,
51
+ so the engine is whichever one the installed invisible_core is sealed to,
52
+ and the core verifies that engine before it hands back a binary. The
53
+ per-profile ``binary_ver`` pin that used to be passed here was retired on
54
+ 2026-07-25: it let one profile run an old engine under the new engine's
55
+ claimed identity, with nothing on screen to say so."""
56
+ rproxy = resolve_launch_proxy(profile.proxy, base) # SX intent -> concrete endpoint
57
+ pdir = paths.profile_dir(profile.id, base=base)
58
+ return _core_build_launch_plan(
59
+ profile.seed,
60
+ profile_dir=pdir,
61
+ proxy=rproxy,
62
+ timezone=profile.timezone,
63
+ locale=profile.locale,
64
+ pin=profile.pin,
65
+ )
66
+
67
+
68
+ # ─────────────────────────────────────────────────────────────────────────
69
+ # Cross-OS process tracking (Win/Linux/mac)
70
+ # ─────────────────────────────────────────────────────────────────────────
71
+ # The pid we get from Popen is NOT always the browser:
72
+ # Windows - `firefox.exe` is a "launcher process" that spawns the real
73
+ # browser as its FIRST CHILD and then EXITS (Mozilla design, see
74
+ # wiki.mozilla.org/.../Launcher_Process). So Popen's pid dies in
75
+ # ~1s and the browser lives under a different pid.
76
+ # Linux - the `firefox` script `exec`s firefox-bin in place: Popen's pid
77
+ # IS the browser.
78
+ # macOS - the launched binary IS the browser (no launcher process).
79
+ # We resolve the real browser uniformly by the PROFILE DIR in its cmdline
80
+ # (unique per profile, present on every OS) and track THAT via psutil, so
81
+ # running-status and stop work identically everywhere.
82
+
83
+
84
+ def _is_main_firefox(p: "psutil.Process", target: str) -> bool:
85
+ """True iff psutil process ``p`` is a MAIN firefox browser (not a content
86
+ child) launched with ``-profile <target>``."""
87
+ try:
88
+ cmd = p.cmdline()
89
+ name = (p.name() or "").lower()
90
+ except Exception:
91
+ return False
92
+ if not cmd or "-contentproc" in cmd:
93
+ return False
94
+ exe0 = (cmd[0] or "").lower()
95
+ if not (name.startswith("firefox") or "firefox" in exe0):
96
+ return False
97
+ return any(target in (c or "") for c in cmd)
98
+
99
+
100
+ def _has_contentproc_child(p: "psutil.Process") -> bool:
101
+ """True if ``p`` has at least one ``-contentproc`` firefox child. The REAL
102
+ browser spawns content processes; the Windows launcher stub does not - so
103
+ this cleanly tells the browser from the stub even while both exist."""
104
+ try:
105
+ for c in p.children(recursive=False):
106
+ try:
107
+ if "-contentproc" in c.cmdline():
108
+ return True
109
+ except Exception:
110
+ continue
111
+ except Exception:
112
+ pass
113
+ return False
114
+
115
+
116
+ def resolve_browser_process(profile_dir: Path, popen_proc: Any, timeout: float = 8.0) -> Any:
117
+ """Return the psutil.Process of the REAL browser for this profile, or None.
118
+
119
+ The browser is the main firefox process (``-profile <dir>``, not a
120
+ ``-contentproc`` child) that HAS content-process children. That test is what
121
+ makes this reliable cross-OS: on Windows the launcher stub also matches the
122
+ ``-profile`` filter but never spawns content children, so it is never
123
+ mistaken for the browser; on Linux/macOS the launched process is the browser
124
+ and has them. Falls back to any alive main-firefox match if content children
125
+ have not appeared before the timeout."""
126
+ if psutil is None:
127
+ return None
128
+ target = str(profile_dir)
129
+ launched_pid = getattr(popen_proc, "pid", None)
130
+ start = time.monotonic()
131
+ fallback = None
132
+ while time.monotonic() - start < timeout:
133
+ candidates: List[Any] = []
134
+ seen = set()
135
+ # Prefer the stub's subtree (Windows), then a global scan (Linux/mac +
136
+ # re-parented Windows child once the stub has exited).
137
+ if launched_pid is not None:
138
+ try:
139
+ for c in psutil.Process(launched_pid).children(recursive=True):
140
+ if _is_main_firefox(c, target):
141
+ candidates.append(c)
142
+ seen.add(c.pid)
143
+ except Exception:
144
+ pass
145
+ try:
146
+ for p in psutil.process_iter(["name"]):
147
+ if not (p.info.get("name") or "").lower().startswith("firefox"):
148
+ continue
149
+ if p.pid in seen:
150
+ continue
151
+ if _is_main_firefox(p, target):
152
+ candidates.append(p)
153
+ except Exception:
154
+ pass
155
+ for c in candidates:
156
+ if _has_contentproc_child(c):
157
+ return c # unambiguously the real browser
158
+ alive = [c for c in candidates if c.is_running()]
159
+ if alive:
160
+ fallback = alive[0]
161
+ time.sleep(0.25)
162
+ return fallback
163
+
164
+
165
+ def process_alive(psproc: Any) -> bool:
166
+ """Cross-OS liveness for a psutil.Process (False if gone or a zombie)."""
167
+ if psproc is None or psutil is None:
168
+ return False
169
+ try:
170
+ return psproc.is_running() and psproc.status() != psutil.STATUS_ZOMBIE
171
+ except Exception:
172
+ return False
173
+
174
+
175
+ def terminate_process_tree(psproc: Any, timeout: float = 5.0) -> None:
176
+ """Terminate a browser and all its child processes, cross-OS. Graceful
177
+ terminate() first, then kill() any survivors."""
178
+ if psproc is None or psutil is None:
179
+ return
180
+ try:
181
+ procs = psproc.children(recursive=True)
182
+ except Exception:
183
+ procs = []
184
+ procs.append(psproc)
185
+ for p in procs:
186
+ try:
187
+ p.terminate()
188
+ except Exception:
189
+ pass
190
+ try:
191
+ _gone, alive = psutil.wait_procs(procs, timeout=timeout)
192
+ except Exception:
193
+ alive = procs
194
+ for p in alive:
195
+ try:
196
+ p.kill()
197
+ except Exception:
198
+ pass
199
+
200
+
201
+ def launch(
202
+ profile: Profile,
203
+ base: Optional[Path] = None,
204
+ spawn: Callable[..., Any] = subprocess.Popen,
205
+ ) -> LaunchHandle:
206
+ plan = build_launch_plan(profile, base=base)
207
+ proc = spawn(plan.argv, env=plan.env)
208
+ # Only resolve the real browser on a REAL launch; an injected spawn (tests)
209
+ # keeps the raw process so its pid/handle stay predictable.
210
+ browser = None
211
+ real_pid = proc.pid
212
+ if spawn is subprocess.Popen:
213
+ browser = resolve_browser_process(plan.profile_dir, proc)
214
+ if browser is not None:
215
+ real_pid = browser.pid
216
+ return LaunchHandle(pid=real_pid, profile_id=profile.id, process=proc, browser=browser)