-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathadmin.py
More file actions
162 lines (137 loc) · 6.38 KB
/
Copy pathadmin.py
File metadata and controls
162 lines (137 loc) · 6.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"""Windows UAC elevation helpers.
Patching the binaries under ``C:\\Program Files\\BlueStacks_nxt`` and terminating
BlueStacks processes both require administrator rights. These helpers let the
app detect whether it is elevated and, if not, relaunch itself through a UAC
prompt.
Network/shared-drive note
-------------------------
An elevated process runs in a different logon session that does NOT inherit the
user's mapped drive letters (e.g. a VMware shared folder mapped to ``Y:``). If
the app is launched from such a drive, a naive relaunch of ``Y:\\...\\main.py``
fails because ``Y:`` doesn't exist for the elevated process, so it never starts.
We therefore rewrite the relaunch path to its UNC form
(``\\\\vmware-host\\Shared Folders\\...``), which the elevated session can reach.
macOS note
----------
None of this applies to BlueStacks Air. Air keeps its config, instance disks
and logs under ``/Users/Shared``, all writable by the logged-in user, so the app
runs unelevated and elevates only for the single operation that needs it -- see
``platform_support.run_elevated``. The functions here degrade to "already fine,
carry on" rather than being absent, so ``main.py`` can call ``ensure_admin()``
unconditionally.
"""
from __future__ import annotations
import logging
import os
import subprocess
import sys
import platform_support
# ctypes.wintypes raises ValueError on import on non-Windows, so both the
# Windows-only imports are conditional.
if platform_support.IS_WINDOWS:
import ctypes
from ctypes import wintypes
logger = logging.getLogger(__name__)
SW_SHOWNORMAL = 1
ERROR_SUCCESS = 0
def is_admin() -> bool:
"""Return True if the current process is running with administrator rights."""
if not platform_support.IS_WINDOWS:
# There is no session-wide elevation to detect on macOS; report the
# truth (are we root?) without implying the app ought to be.
return os.geteuid() == 0
try:
return bool(ctypes.windll.shell32.IsUserAnAdmin())
except Exception: # noqa: BLE001 - any failure means "assume not admin"
logger.debug("IsUserAnAdmin() check failed", exc_info=True)
return False
def _drive_unc_root(drive: str) -> str | None:
"""Return the UNC root for a mapped drive (e.g. 'Y:' -> r'\\\\host\\share'), or None."""
try:
mpr = ctypes.WinDLL("mpr", use_last_error=True)
buf = ctypes.create_unicode_buffer(1024)
length = wintypes.DWORD(1024)
rc = mpr.WNetGetConnectionW(drive, buf, ctypes.byref(length))
if rc == ERROR_SUCCESS and buf.value:
return buf.value
except Exception: # noqa: BLE001
logger.debug("WNetGetConnectionW failed for %s", drive, exc_info=True)
return None
def to_accessible_path(path: str) -> str:
"""Rewrite a path on a mapped network drive to its UNC form.
Elevated processes can't see mapped drive letters but can reach the
underlying UNC share, so this keeps relaunch-as-admin working when the app
lives on a VMware shared folder / network drive. Non-network paths are
returned unchanged.
"""
abspath = os.path.abspath(path)
drive, rest = os.path.splitdrive(abspath)
if len(drive) == 2 and drive[1] == ":":
try:
# DriveType 4 == network (DRIVE_REMOTE)
if ctypes.windll.kernel32.GetDriveTypeW(drive + "\\") == 4:
unc_root = _drive_unc_root(drive)
if unc_root:
return unc_root + rest
except Exception: # noqa: BLE001
logger.debug("Drive-type check failed for %s", drive, exc_info=True)
return abspath
def relaunch_as_admin() -> bool:
"""Relaunch the current program elevated via a UAC prompt.
Works both from source (``python main.py ...``) and as a frozen PyInstaller
executable, and from a mapped network/shared drive (rewritten to UNC).
Returns True if an elevated instance was started (caller should then exit),
or False if elevation was declined or failed.
"""
if not platform_support.IS_WINDOWS:
# No session-wide elevation on macOS -- see the module docstring.
return False
if is_admin():
return False
if getattr(sys, "frozen", False):
# Frozen exe: relaunch the exe itself with the same args.
executable = to_accessible_path(sys.executable)
params = subprocess.list2cmdline(sys.argv[1:])
workdir = os.path.dirname(executable)
else:
# Running from source: relaunch the interpreter with this script + args.
# The script path is made UNC-safe; the interpreter itself is local.
script = to_accessible_path(sys.argv[0])
executable = sys.executable
params = subprocess.list2cmdline([script] + sys.argv[1:])
workdir = os.path.dirname(script)
# A UNC working directory is unreliable; the script's own directory is added
# to sys.path[0] from the absolute script path anyway, so cwd isn't needed
# for imports. Pass None when workdir is UNC to avoid ShellExecute quirks.
lp_directory = None if workdir.startswith("\\\\") else workdir
try:
# ShellExecuteW with the "runas" verb raises the UAC consent dialog.
rc = ctypes.windll.shell32.ShellExecuteW(
None, "runas", executable, params, lp_directory, SW_SHOWNORMAL
)
except Exception: # noqa: BLE001
logger.exception("Failed to request elevation")
return False
# ShellExecuteW returns a value > 32 on success.
if int(rc) > 32:
logger.info("Relaunched elevated (%s %s); exiting unelevated instance.",
executable, params)
return True
logger.warning("Elevation request was declined or failed (code %s).", rc)
return False
def ensure_admin() -> None:
"""Elevate if needed: if not admin, request elevation and exit this process.
If the user accepts the UAC prompt an elevated copy is launched and this
(unelevated) process exits. If they decline, this process keeps running so
the app still opens -- it will simply surface permission errors when it tries
to patch or kill processes.
On macOS this is a no-op: the app is meant to run as the logged-in user and
elevates per-operation instead (``platform_support.run_elevated``).
"""
if not platform_support.IS_WINDOWS:
return
if is_admin():
return
if relaunch_as_admin():
sys.exit(0)
logger.warning("Continuing without administrator rights.")