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.
- invisible_firefox-0.2.0/.gitignore +7 -0
- invisible_firefox-0.2.0/LICENSE +21 -0
- invisible_firefox-0.2.0/PKG-INFO +91 -0
- invisible_firefox-0.2.0/README.md +56 -0
- invisible_firefox-0.2.0/pyproject.toml +94 -0
- invisible_firefox-0.2.0/src/invisible_firefox/__init__.py +88 -0
- invisible_firefox-0.2.0/src/invisible_firefox/__main__.py +4 -0
- invisible_firefox-0.2.0/src/invisible_firefox/manager/__init__.py +0 -0
- invisible_firefox-0.2.0/src/invisible_firefox/manager/fingerprint.py +36 -0
- invisible_firefox-0.2.0/src/invisible_firefox/manager/launcher.py +216 -0
- invisible_firefox-0.2.0/src/invisible_firefox/manager/models.py +52 -0
- invisible_firefox-0.2.0/src/invisible_firefox/manager/paths.py +29 -0
- invisible_firefox-0.2.0/src/invisible_firefox/manager/settings.py +33 -0
- invisible_firefox-0.2.0/src/invisible_firefox/manager/store.py +89 -0
- invisible_firefox-0.2.0/src/invisible_firefox/manager/sx.py +157 -0
- invisible_firefox-0.2.0/src/invisible_firefox/py.typed +0 -0
- invisible_firefox-0.2.0/src/invisible_firefox/ui/__init__.py +0 -0
- invisible_firefox-0.2.0/src/invisible_firefox/ui/app.py +7 -0
- invisible_firefox-0.2.0/src/invisible_firefox/ui/web/index.html +690 -0
- invisible_firefox-0.2.0/src/invisible_firefox/ui/web_app.py +346 -0
- invisible_firefox-0.2.0/tests/conftest.py +2 -0
- invisible_firefox-0.2.0/tests/test_fingerprint.py +24 -0
- invisible_firefox-0.2.0/tests/test_launcher.py +103 -0
- invisible_firefox-0.2.0/tests/test_models.py +18 -0
- invisible_firefox-0.2.0/tests/test_paths.py +11 -0
- invisible_firefox-0.2.0/tests/test_seal_floor.py +371 -0
- invisible_firefox-0.2.0/tests/test_status.py +48 -0
- invisible_firefox-0.2.0/tests/test_store.py +59 -0
- invisible_firefox-0.2.0/tests/test_sx.py +110 -0
- invisible_firefox-0.2.0/tests/test_web_api.py +69 -0
|
@@ -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
|
+
<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
|
+
<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)
|
|
File without changes
|
|
@@ -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)
|