├── .gitattributes ├── .gitignore ├── LICENSE ├── PatchMerge.bat ├── PatchMerge.command ├── PatchMerge.py ├── README.md ├── SSDTTime.bat ├── SSDTTime.command ├── SSDTTime.py └── Scripts ├── __init__.py ├── downloader.py ├── dsdt.py ├── plist.py ├── reveal.py ├── run.py └── utils.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ensure all .bat scripts use CRLF line endings 2 | # This can prevent a number of odd batch issues 3 | *.bat text eol=crlf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | Results/* 107 | iasl* 108 | acpidump* 109 | 110 | .vs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 CorpNewt 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. 22 | -------------------------------------------------------------------------------- /PatchMerge.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Get our local path and args before delayed expansion - allows % and ! 3 | set "thisDir=%~dp0" 4 | set "args=%*" 5 | 6 | setlocal enableDelayedExpansion 7 | REM Setup initial vars 8 | set "script_name=" 9 | set /a tried=0 10 | set "toask=yes" 11 | set "pause_on_error=yes" 12 | set "py2v=" 13 | set "py2path=" 14 | set "py3v=" 15 | set "py3path=" 16 | set "pypath=" 17 | set "targetpy=3" 18 | 19 | REM use_py3: 20 | REM TRUE = Use if found, use py2 otherwise 21 | REM FALSE = Use py2 22 | REM FORCE = Use py3 23 | set "use_py3=TRUE" 24 | 25 | REM We'll parse if the first argument passed is 26 | REM --install-python and if so, we'll just install 27 | REM Can optionally take a version number as the 28 | REM second arg - i.e. --install-python 3.13.1 29 | set "just_installing=FALSE" 30 | set "user_provided=" 31 | 32 | REM Get the system32 (or equivalent) path 33 | call :getsyspath "syspath" 34 | 35 | REM Make sure the syspath exists 36 | if "!syspath!" == "" ( 37 | if exist "%SYSTEMROOT%\system32\cmd.exe" ( 38 | if exist "%SYSTEMROOT%\system32\reg.exe" ( 39 | if exist "%SYSTEMROOT%\system32\where.exe" ( 40 | REM Fall back on the default path if it exists 41 | set "ComSpec=%SYSTEMROOT%\system32\cmd.exe" 42 | set "syspath=%SYSTEMROOT%\system32\" 43 | ) 44 | ) 45 | ) 46 | if "!syspath!" == "" ( 47 | cls 48 | echo ### ### 49 | echo # Missing Required Files # 50 | echo ### ### 51 | echo. 52 | echo Could not locate cmd.exe, reg.exe, or where.exe 53 | echo. 54 | echo Please ensure your ComSpec environment variable is properly configured and 55 | echo points directly to cmd.exe, then try again. 56 | echo. 57 | echo Current CompSpec Value: "%ComSpec%" 58 | echo. 59 | echo Press [enter] to quit. 60 | pause > nul 61 | exit /b 1 62 | ) 63 | ) 64 | 65 | if "%~1" == "--install-python" ( 66 | set "just_installing=TRUE" 67 | set "user_provided=%~2" 68 | goto installpy 69 | ) 70 | 71 | goto checkscript 72 | 73 | :checkscript 74 | REM Check for our script first 75 | set "looking_for=!script_name!" 76 | if "!script_name!" == "" ( 77 | set "looking_for=%~n0.py or %~n0.command" 78 | set "script_name=%~n0.py" 79 | if not exist "!thisDir!\!script_name!" ( 80 | set "script_name=%~n0.command" 81 | ) 82 | ) 83 | if not exist "!thisDir!\!script_name!" ( 84 | cls 85 | echo ### ### 86 | echo # Target Not Found # 87 | echo ### ### 88 | echo. 89 | echo Could not find !looking_for!. 90 | echo Please make sure to run this script from the same directory 91 | echo as !looking_for!. 92 | echo. 93 | echo Press [enter] to quit. 94 | pause > nul 95 | exit /b 1 96 | ) 97 | goto checkpy 98 | 99 | :checkpy 100 | call :updatepath 101 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 102 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 103 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher "%%x" "py2v" "py2path" "py3v" "py3path" ) 104 | REM Walk our returns to see if we need to install 105 | if /i "!use_py3!" == "FALSE" ( 106 | set "targetpy=2" 107 | set "pypath=!py2path!" 108 | ) else if /i "!use_py3!" == "FORCE" ( 109 | set "pypath=!py3path!" 110 | ) else if /i "!use_py3!" == "TRUE" ( 111 | set "pypath=!py3path!" 112 | if "!pypath!" == "" set "pypath=!py2path!" 113 | ) 114 | if not "!pypath!" == "" ( 115 | goto runscript 116 | ) 117 | if !tried! lss 1 ( 118 | if /i "!toask!"=="yes" ( 119 | REM Better ask permission first 120 | goto askinstall 121 | ) else ( 122 | goto installpy 123 | ) 124 | ) else ( 125 | cls 126 | echo ### ### 127 | echo # Python Not Found # 128 | echo ### ### 129 | echo. 130 | REM Couldn't install for whatever reason - give the error message 131 | echo Python is not installed or not found in your PATH var. 132 | echo Please go to https://www.python.org/downloads/windows/ to 133 | echo download and install the latest version, then try again. 134 | echo. 135 | echo Make sure you check the box labeled: 136 | echo. 137 | echo "Add Python X.X to PATH" 138 | echo. 139 | echo Where X.X is the py version you're installing. 140 | echo. 141 | echo Press [enter] to quit. 142 | pause > nul 143 | exit /b 1 144 | ) 145 | goto runscript 146 | 147 | :checkpylauncher 148 | REM Attempt to check the latest python 2 and 3 versions via the py launcher 149 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -2 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 150 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -3 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 151 | goto :EOF 152 | 153 | :checkpyversion 154 | set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) do ( 155 | REM Ensure we have a version number 156 | call :isnumber "%%a" 157 | if not "!errorlevel!" == "0" goto :EOF 158 | set "version=%%a" 159 | ) 160 | if not defined version goto :EOF 161 | if "!version:~0,1!" == "2" ( 162 | REM Python 2 163 | call :comparepyversion "!version!" "!%~2!" 164 | if "!errorlevel!" == "1" ( 165 | set "%~2=!version!" 166 | set "%~3=%~1" 167 | ) 168 | ) else ( 169 | REM Python 3 170 | call :comparepyversion "!version!" "!%~4!" 171 | if "!errorlevel!" == "1" ( 172 | set "%~4=!version!" 173 | set "%~5=%~1" 174 | ) 175 | ) 176 | goto :EOF 177 | 178 | :isnumber 179 | set "var="&for /f "delims=0123456789." %%i in ("%~1") do set var=%%i 180 | if defined var (exit /b 1) 181 | exit /b 0 182 | 183 | :comparepyversion 184 | REM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2 185 | for /f "tokens=1,2,3 delims=." %%a in ("%~1") do ( 186 | set a1=%%a 187 | set a2=%%b 188 | set a3=%%c 189 | ) 190 | for /f "tokens=1,2,3 delims=." %%a in ("%~2") do ( 191 | set b1=%%a 192 | set b2=%%b 193 | set b3=%%c 194 | ) 195 | if not defined a1 set a1=0 196 | if not defined a2 set a2=0 197 | if not defined a3 set a3=0 198 | if not defined b1 set b1=0 199 | if not defined b2 set b2=0 200 | if not defined b3 set b3=0 201 | if %a1% gtr %b1% exit /b 1 202 | if %a1% lss %b1% exit /b 2 203 | if %a2% gtr %b2% exit /b 1 204 | if %a2% lss %b2% exit /b 2 205 | if %a3% gtr %b3% exit /b 1 206 | if %a3% lss %b3% exit /b 2 207 | exit /b 0 208 | 209 | :askinstall 210 | cls 211 | echo ### ### 212 | echo # Python Not Found # 213 | echo ### ### 214 | echo. 215 | echo Python !targetpy! was not found on the system or in the PATH var. 216 | echo. 217 | set /p "menu=Would you like to install it now? [y/n]: " 218 | if /i "!menu!"=="y" ( 219 | REM We got the OK - install it 220 | goto installpy 221 | ) else if "!menu!"=="n" ( 222 | REM No OK here... 223 | set /a tried=!tried!+1 224 | goto checkpy 225 | ) 226 | REM Incorrect answer - go back 227 | goto askinstall 228 | 229 | :installpy 230 | REM This will attempt to download and install python 231 | set /a tried=!tried!+1 232 | cls 233 | echo ### ### 234 | echo # Downloading Python # 235 | echo ### ### 236 | echo. 237 | set "release=!user_provided!" 238 | if "!release!" == "" ( 239 | REM No explicit release set - get the latest from python.org 240 | echo Gathering latest version... 241 | powershell -command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;(new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\pyurl.txt')" 242 | REM Extract it if it's gzip compressed 243 | powershell -command "$infile='%TEMP%\pyurl.txt';$outfile='%TEMP%\pyurl.temp';try{$input=New-Object System.IO.FileStream $infile,([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read);$output=New-Object System.IO.FileStream $outfile,([IO.FileMode]::Create),([IO.FileAccess]::Write),([IO.FileShare]::None);$gzipStream=New-Object System.IO.Compression.GzipStream $input,([IO.Compression.CompressionMode]::Decompress);$buffer=New-Object byte[](1024);while($true){$read=$gzipstream.Read($buffer,0,1024);if($read -le 0){break};$output.Write($buffer,0,$read)};$gzipStream.Close();$output.Close();$input.Close();Move-Item -Path $outfile -Destination $infile -Force}catch{}" 244 | if not exist "%TEMP%\pyurl.txt" ( 245 | if /i "!just_installing!" == "TRUE" ( 246 | echo - Failed to get info 247 | exit /b 1 248 | ) else ( 249 | goto checkpy 250 | ) 251 | ) 252 | pushd "%TEMP%" 253 | :: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20) 254 | for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python !targetpy! Release" pyurl.txt') do ( set "release=%%x" ) 255 | popd 256 | REM Let's delete our txt file now - we no longer need it 257 | del "%TEMP%\pyurl.txt" 258 | if "!release!" == "" ( 259 | if /i "!just_installing!" == "TRUE" ( 260 | echo - Failed to get python version 261 | exit /b 1 262 | ) else ( 263 | goto checkpy 264 | ) 265 | ) 266 | echo Located Version: !release! 267 | ) else ( 268 | echo User-Provided Version: !release! 269 | REM Update our targetpy to reflect the first number of 270 | REM our release 271 | for /f "tokens=1 delims=." %%a in ("!release!") do ( 272 | call :isnumber "%%a" 273 | if "!errorlevel!" == "0" ( 274 | set "targetpy=%%a" 275 | ) 276 | ) 277 | ) 278 | echo Building download url... 279 | REM At this point - we should have the version number. 280 | REM We can build the url like so: "https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe" 281 | set "url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe" 282 | set "pytype=exe" 283 | if "!targetpy!" == "2" ( 284 | set "url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi" 285 | set "pytype=msi" 286 | ) 287 | echo - !url! 288 | echo Downloading... 289 | REM Now we download it with our slick powershell command 290 | powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\pyinstall.!pytype!')" 291 | REM If it doesn't exist - we bail 292 | if not exist "%TEMP%\pyinstall.!pytype!" ( 293 | if /i "!just_installing!" == "TRUE" ( 294 | echo - Failed to download python installer 295 | exit /b 1 296 | ) else ( 297 | goto checkpy 298 | ) 299 | ) 300 | REM It should exist at this point - let's run it to install silently 301 | echo Running python !pytype! installer... 302 | pushd "%TEMP%" 303 | if /i "!pytype!" == "exe" ( 304 | echo - pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 305 | pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 306 | ) else ( 307 | set "foldername=!release:.=!" 308 | echo - msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 309 | msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 310 | ) 311 | popd 312 | set "py_error=!errorlevel!" 313 | echo Installer finished with status: !py_error! 314 | echo Cleaning up... 315 | REM Now we should be able to delete the installer and check for py again 316 | del "%TEMP%\pyinstall.!pytype!" 317 | REM If it worked, then we should have python in our PATH 318 | REM this does not get updated right away though - let's try 319 | REM manually updating the local PATH var 320 | call :updatepath 321 | if /i "!just_installing!" == "TRUE" ( 322 | echo. 323 | echo Done. 324 | ) else ( 325 | goto checkpy 326 | ) 327 | exit /b 328 | 329 | :runscript 330 | REM Python found 331 | cls 332 | REM Checks the args gathered at the beginning of the script. 333 | REM Make sure we're not just forwarding empty quotes. 334 | set "arg_test=!args:"=!" 335 | if "!arg_test!"=="" ( 336 | "!pypath!" "!thisDir!!script_name!" 337 | ) else ( 338 | "!pypath!" "!thisDir!!script_name!" !args! 339 | ) 340 | if /i "!pause_on_error!" == "yes" ( 341 | if not "%ERRORLEVEL%" == "0" ( 342 | echo. 343 | echo Script exited with error code: %ERRORLEVEL% 344 | echo. 345 | echo Press [enter] to exit... 346 | pause > nul 347 | ) 348 | ) 349 | goto :EOF 350 | 351 | :undouble 352 | REM Helper function to strip doubles of a single character out of a string recursively 353 | set "string_value=%~2" 354 | :undouble_continue 355 | set "check=!string_value:%~3%~3=%~3!" 356 | if not "!check!" == "!string_value!" ( 357 | set "string_value=!check!" 358 | goto :undouble_continue 359 | ) 360 | set "%~1=!check!" 361 | goto :EOF 362 | 363 | :updatepath 364 | set "spath=" 365 | set "upath=" 366 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKCU\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "upath=%%j" ) 367 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "spath=%%j" ) 368 | if not "%spath%" == "" ( 369 | REM We got something in the system path 370 | set "PATH=%spath%" 371 | if not "%upath%" == "" ( 372 | REM We also have something in the user path 373 | set "PATH=%PATH%;%upath%" 374 | ) 375 | ) else if not "%upath%" == "" ( 376 | set "PATH=%upath%" 377 | ) 378 | REM Remove double semicolons from the adjusted PATH 379 | call :undouble "PATH" "%PATH%" ";" 380 | goto :EOF 381 | 382 | :getsyspath 383 | REM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe by 384 | REM walking the ComSpec var - will also repair it in memory if need be 385 | REM Strip double semi-colons 386 | call :undouble "temppath" "%ComSpec%" ";" 387 | 388 | REM Dirty hack to leverage the "line feed" approach - there are some odd side 389 | REM effects with this. Do not use this variable name in comments near this 390 | REM line - as it seems to behave erradically. 391 | (set LF=^ 392 | %=this line is empty=% 393 | ) 394 | REM Replace instances of semi-colons with a line feed and wrap 395 | REM in parenthesis to work around some strange batch behavior 396 | set "testpath=%temppath:;=!LF!%" 397 | 398 | REM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there 399 | set /a found=0 400 | for /f "tokens=* delims=" %%i in ("!testpath!") do ( 401 | REM Only continue if we haven't found it yet 402 | if not "%%i" == "" ( 403 | if !found! lss 1 ( 404 | set "checkpath=%%i" 405 | REM Remove "cmd.exe" from the end if it exists 406 | if /i "!checkpath:~-7!" == "cmd.exe" ( 407 | set "checkpath=!checkpath:~0,-7!" 408 | ) 409 | REM Pad the end with a backslash if needed 410 | if not "!checkpath:~-1!" == "\" ( 411 | set "checkpath=!checkpath!\" 412 | ) 413 | REM Let's see if cmd, reg, and where exist there - and set it if so 414 | if EXIST "!checkpath!cmd.exe" ( 415 | if EXIST "!checkpath!reg.exe" ( 416 | if EXIST "!checkpath!where.exe" ( 417 | set /a found=1 418 | set "ComSpec=!checkpath!cmd.exe" 419 | set "%~1=!checkpath!" 420 | ) 421 | ) 422 | ) 423 | ) 424 | ) 425 | ) 426 | goto :EOF 427 | -------------------------------------------------------------------------------- /PatchMerge.command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the curent directory, the script name 4 | # and the script name with "py" substituted for the extension. 5 | args=( "$@" ) 6 | dir="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" 7 | script="${0##*/}" 8 | target="${script%.*}.py" 9 | 10 | # use_py3: 11 | # TRUE = Use if found, use py2 otherwise 12 | # FALSE = Use py2 13 | # FORCE = Use py3 14 | use_py3="TRUE" 15 | 16 | # We'll parse if the first argument passed is 17 | # --install-python and if so, we'll just install 18 | # Can optionally take a version number as the 19 | # second arg - i.e. --install-python 3.13.1 20 | just_installing="FALSE" 21 | 22 | tempdir="" 23 | 24 | compare_to_version () { 25 | # Compares our OS version to the passed OS version, and 26 | # return a 1 if we match the passed compare type, or a 0 if we don't. 27 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 28 | # $2 = OS version to compare ours to 29 | if [ -z "$1" ] || [ -z "$2" ]; then 30 | # Missing info - bail. 31 | return 32 | fi 33 | local current_os= comp= 34 | current_os="$(sw_vers -productVersion 2>/dev/null)" 35 | comp="$(vercomp "$current_os" "$2")" 36 | # Check gequal and lequal first 37 | if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || [[ "$comp" == "$1" ]]; then 38 | # Matched 39 | echo "1" 40 | else 41 | # No match 42 | echo "0" 43 | fi 44 | } 45 | 46 | set_use_py3_if () { 47 | # Auto sets the "use_py3" variable based on 48 | # conditions passed 49 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 50 | # $2 = OS version to compare 51 | # $3 = TRUE/FALSE/FORCE in case of match 52 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 53 | # Missing vars - bail with no changes. 54 | return 55 | fi 56 | if [ "$(compare_to_version "$1" "$2")" == "1" ]; then 57 | use_py3="$3" 58 | fi 59 | } 60 | 61 | get_remote_py_version () { 62 | local pyurl= py_html= py_vers= py_num="3" 63 | pyurl="https://www.python.org/downloads/macos/" 64 | py_html="$(curl -L $pyurl --compressed 2>&1)" 65 | if [ -z "$use_py3" ]; then 66 | use_py3="TRUE" 67 | fi 68 | if [ "$use_py3" == "FALSE" ]; then 69 | py_num="2" 70 | fi 71 | py_vers="$(echo "$py_html" | grep -i "Latest Python $py_num Release" | awk '{print $8}' | cut -d'<' -f1)" 72 | echo "$py_vers" 73 | } 74 | 75 | download_py () { 76 | local vers="$1" url= 77 | clear 78 | echo " ### ###" 79 | echo " # Downloading Python #" 80 | echo "### ###" 81 | echo 82 | if [ -z "$vers" ]; then 83 | echo "Gathering latest version..." 84 | vers="$(get_remote_py_version)" 85 | if [ -z "$vers" ]; then 86 | if [ "$just_installing" == "TRUE" ]; then 87 | echo " - Failed to get info!" 88 | exit 1 89 | else 90 | # Didn't get it still - bail 91 | print_error 92 | fi 93 | fi 94 | echo "Located Version: $vers" 95 | else 96 | # Got a version passed 97 | echo "User-Provided Version: $vers" 98 | fi 99 | echo "Building download url..." 100 | url="$(curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }' | head -n 1)" 101 | if [ -z "$url" ]; then 102 | if [ "$just_installing" == "TRUE" ]; then 103 | echo " - Failed to build download url!" 104 | exit 1 105 | else 106 | # Couldn't get the URL - bail 107 | print_error 108 | fi 109 | fi 110 | echo " - $url" 111 | echo "Downloading..." 112 | # Create a temp dir and download to it 113 | tempdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')" 114 | curl "$url" -o "$tempdir/python.pkg" 115 | if [ "$?" != "0" ]; then 116 | echo " - Failed to download python installer!" 117 | exit $? 118 | fi 119 | echo 120 | echo "Running python install package..." 121 | echo 122 | sudo installer -pkg "$tempdir/python.pkg" -target / 123 | echo 124 | if [ "$?" != "0" ]; then 125 | echo " - Failed to install python!" 126 | exit $? 127 | fi 128 | # Now we expand the package and look for a shell update script 129 | pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" 130 | if [ -e "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" ]; then 131 | # Run the script 132 | echo "Updating PATH..." 133 | echo 134 | "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" 135 | echo 136 | fi 137 | vers_folder="Python $(echo "$vers" | cut -d'.' -f1 -f2)" 138 | if [ -f "/Applications/$vers_folder/Install Certificates.command" ]; then 139 | # Certs script exists - let's execute that to make sure our certificates are updated 140 | echo "Updating Certificates..." 141 | echo 142 | "/Applications/$vers_folder/Install Certificates.command" 143 | echo 144 | fi 145 | echo "Cleaning up..." 146 | cleanup 147 | if [ "$just_installing" == "TRUE" ]; then 148 | echo 149 | echo "Done." 150 | else 151 | # Now we check for py again 152 | downloaded="TRUE" 153 | clear 154 | main 155 | fi 156 | } 157 | 158 | cleanup () { 159 | if [ -d "$tempdir" ]; then 160 | rm -Rf "$tempdir" 161 | fi 162 | } 163 | 164 | print_error() { 165 | clear 166 | cleanup 167 | echo " ### ###" 168 | echo " # Python Not Found #" 169 | echo "### ###" 170 | echo 171 | echo "Python is not installed or not found in your PATH var." 172 | echo 173 | if [ "$kernel" == "Darwin" ]; then 174 | echo "Please go to https://www.python.org/downloads/macos/ to" 175 | echo "download and install the latest version, then try again." 176 | else 177 | echo "Please install python through your package manager and" 178 | echo "try again." 179 | fi 180 | echo 181 | exit 1 182 | } 183 | 184 | print_target_missing() { 185 | clear 186 | cleanup 187 | echo " ### ###" 188 | echo " # Target Not Found #" 189 | echo "### ###" 190 | echo 191 | echo "Could not locate $target!" 192 | echo 193 | exit 1 194 | } 195 | 196 | format_version () { 197 | local vers="$1" 198 | echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" 199 | } 200 | 201 | vercomp () { 202 | # Modified from: https://apple.stackexchange.com/a/123408/11374 203 | local ver1="$(format_version "$1")" ver2="$(format_version "$2")" 204 | if [ $ver1 -gt $ver2 ]; then 205 | echo "1" 206 | elif [ $ver1 -lt $ver2 ]; then 207 | echo "2" 208 | else 209 | echo "0" 210 | fi 211 | } 212 | 213 | get_local_python_version() { 214 | # $1 = Python bin name (defaults to python3) 215 | # Echoes the path to the highest version of the passed python bin if any 216 | local py_name="$1" max_version= python= python_version= python_path= 217 | if [ -z "$py_name" ]; then 218 | py_name="python3" 219 | fi 220 | py_list="$(which -a "$py_name" 2>/dev/null)" 221 | # Walk that newline separated list 222 | while read python; do 223 | if [ -z "$python" ]; then 224 | # Got a blank line - skip 225 | continue 226 | fi 227 | if [ "$check_py3_stub" == "1" ] && [ "$python" == "/usr/bin/python3" ]; then 228 | # See if we have a valid developer path 229 | xcode-select -p > /dev/null 2>&1 230 | if [ "$?" != "0" ]; then 231 | # /usr/bin/python3 path - but no valid developer dir 232 | continue 233 | fi 234 | fi 235 | python_version="$(get_python_version $python)" 236 | if [ -z "$python_version" ]; then 237 | # Didn't find a py version - skip 238 | continue 239 | fi 240 | # Got the py version - compare to our max 241 | if [ -z "$max_version" ] || [ "$(vercomp "$python_version" "$max_version")" == "1" ]; then 242 | # Max not set, or less than the current - update it 243 | max_version="$python_version" 244 | python_path="$python" 245 | fi 246 | done <<< "$py_list" 247 | echo "$python_path" 248 | } 249 | 250 | get_python_version() { 251 | local py_path="$1" py_version= 252 | # Get the python version by piping stderr into stdout (for py2), then grepping the output for 253 | # the word "python", getting the second element, and grepping for an alphanumeric version number 254 | py_version="$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[A-Za-z\d\.]+")" 255 | if [ ! -z "$py_version" ]; then 256 | echo "$py_version" 257 | fi 258 | } 259 | 260 | prompt_and_download() { 261 | if [ "$downloaded" != "FALSE" ] || [ "$kernel" != "Darwin" ]; then 262 | # We already tried to download, or we're not on macOS - just bail 263 | print_error 264 | fi 265 | clear 266 | echo " ### ###" 267 | echo " # Python Not Found #" 268 | echo "### ###" 269 | echo 270 | target_py="Python 3" 271 | printed_py="Python 2 or 3" 272 | if [ "$use_py3" == "FORCE" ]; then 273 | printed_py="Python 3" 274 | elif [ "$use_py3" == "FALSE" ]; then 275 | target_py="Python 2" 276 | printed_py="Python 2" 277 | fi 278 | echo "Could not locate $printed_py!" 279 | echo 280 | echo "This script requires $printed_py to run." 281 | echo 282 | while true; do 283 | read -p "Would you like to install the latest $target_py now? (y/n): " yn 284 | case $yn in 285 | [Yy]* ) download_py;break;; 286 | [Nn]* ) print_error;; 287 | esac 288 | done 289 | } 290 | 291 | main() { 292 | local python= version= 293 | # Verify our target exists 294 | if [ ! -f "$dir/$target" ]; then 295 | # Doesn't exist 296 | print_target_missing 297 | fi 298 | if [ -z "$use_py3" ]; then 299 | use_py3="TRUE" 300 | fi 301 | if [ "$use_py3" != "FALSE" ]; then 302 | # Check for py3 first 303 | python="$(get_local_python_version python3)" 304 | fi 305 | if [ "$use_py3" != "FORCE" ] && [ -z "$python" ]; then 306 | # We aren't using py3 explicitly, and we don't already have a path 307 | python="$(get_local_python_version python2)" 308 | if [ -z "$python" ]; then 309 | # Try just looking for "python" 310 | python="$(get_local_python_version python)" 311 | fi 312 | fi 313 | if [ -z "$python" ]; then 314 | # Didn't ever find it - prompt 315 | prompt_and_download 316 | return 1 317 | fi 318 | # Found it - start our script and pass all args 319 | "$python" "$dir/$target" "${args[@]}" 320 | } 321 | 322 | # Keep track of whether or not we're on macOS to determine if 323 | # we can download and install python for the user as needed. 324 | kernel="$(uname -s)" 325 | # Check to see if we need to force based on 326 | # macOS version. 10.15 has a dummy python3 version 327 | # that can trip up some py3 detection in other scripts. 328 | # set_use_py3_if "3" "10.15" "FORCE" 329 | downloaded="FALSE" 330 | # Check for the aforementioned /usr/bin/python3 stub if 331 | # our OS version is 10.15 or greater. 332 | check_py3_stub="$(compare_to_version "3" "10.15")" 333 | trap cleanup EXIT 334 | if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then 335 | just_installing="TRUE" 336 | download_py "$2" 337 | else 338 | main 339 | fi 340 | -------------------------------------------------------------------------------- /PatchMerge.py: -------------------------------------------------------------------------------- 1 | from Scripts import utils, plist 2 | import argparse, os 3 | 4 | class PatchMerge: 5 | def __init__(self, config=None, results=None, overwrite=False, interactive=True): 6 | self.u = utils.Utils("Patch Merge") 7 | self.w = 80 8 | self.h = 24 9 | self.red = "\u001b[41;1m" 10 | self.yel = "\u001b[43;1m" 11 | self.grn = "\u001b[42;1m" 12 | self.blu = "\u001b[46;1m" 13 | self.rst = "\u001b[0m" 14 | self.copy_as_path = self.u.check_admin() if os.name=="nt" else False 15 | if 2/3==0: 16 | # ANSI escapes don't seem to work properly with python 2.x 17 | self.red = self.yel = self.grn = self.blu = self.rst = "" 18 | if os.name == "nt": 19 | if 2/3!=0: 20 | os.system("color") # Allow ANSI color escapes. 21 | self.w = 120 22 | self.h = 30 23 | self.interactive = interactive 24 | self.overwrite = overwrite 25 | self.target_patches = ( 26 | ("OpenCore","patches_OC.plist"), 27 | ("Clover","patches_Clover.plist") 28 | ) 29 | self.config_path = config 30 | self.config_type = None 31 | self.output = results or self.get_default_results_folder() 32 | # Expand paths as needed 33 | if self.config_path: 34 | self.config_path = os.path.realpath(self.config_path) 35 | self.config_type,_,_ = self.get_plist_info(self.config_path) 36 | if self.output: 37 | self.output = os.path.realpath(self.output) 38 | 39 | def _get_patches_plists(self, path): 40 | # Append patches_OC/Clover.plist to the path, and return a list 41 | # with the format: 42 | # ((oc_path,exists,plist_name),(clover_path,exists,plist_name)) 43 | path_checks = [] 44 | for p_type,name in self.target_patches: 45 | if path: 46 | p = os.path.join(path,name) 47 | isfile = os.path.isfile(p) 48 | else: 49 | p = None 50 | isfile = False 51 | path_checks.append(( 52 | p, 53 | isfile, 54 | name 55 | )) 56 | return path_checks 57 | 58 | def get_default_results_folder(self, prompt=False): 59 | # Let's attempt to locate a Results folder either in the same 60 | # directory as this script, or in the parent directory. 61 | # If none is found - we'll have to prompt the user as needed. 62 | # 63 | # Try our directory first 64 | local_path = os.path.dirname(os.path.realpath(__file__)) 65 | local_results = os.path.join(local_path,"Results") 66 | parent_results = os.path.realpath(os.path.join(local_path,"..","Results")) 67 | potentials = [] 68 | for path in (local_results,parent_results): 69 | if os.path.isdir(path): 70 | # Check if we have the files we need 71 | o,c = self._get_patches_plists(path) 72 | if o[1] or c[1]: 73 | potentials.append(path) 74 | if potentials: 75 | return potentials[0] 76 | # If we got here - we didn't find anything - check if we need 77 | # to prompt 78 | if not prompt: 79 | # Nope - bail 80 | return None 81 | # We're prompting 82 | return self.select_results_folder() 83 | 84 | def select_results_folder(self): 85 | while True: 86 | self.u.head("Select Results Folder") 87 | print("") 88 | if self.copy_as_path: 89 | print("NOTE: Currently running as admin on Windows - drag and drop may not work.") 90 | print(" Shift + right-click in Explorer and select 'Copy as path' then paste here instead.") 91 | print("") 92 | print("M. Main Menu") 93 | print("Q. Quit") 94 | print("") 95 | print("NOTE: This is the folder containing the patches_OC.plist and") 96 | print(" patches_Clover.plist you are trying to merge. It will also be where") 97 | print(" the patched config.plist is saved.") 98 | print("") 99 | path = self.u.grab("Please drag and drop the Results folder here: ") 100 | if not path: 101 | continue 102 | if path.lower() == "m": 103 | return self.output 104 | elif path.lower() == "q": 105 | self.u.custom_quit() 106 | test_path = self.u.check_path(path) 107 | if os.path.isfile(test_path): 108 | # Got a file - get the containing folder 109 | test_path = os.path.dirname(test_path) 110 | if not test_path: 111 | self.u.head("Invalid Path") 112 | print("") 113 | print("That path either does not exist, or is not a folder.") 114 | print("") 115 | self.u.grab("Press [enter] to return...") 116 | continue 117 | # Got a folder - check for patches_OC/Clover.plist 118 | o,c = self._get_patches_plists(test_path) 119 | if not (o[1] or c[1]): 120 | # No patches plists in there 121 | self.u.head("Missing Files") 122 | print("") 123 | print("Neither patches_OC.plist nor patches_Clover.plist were found at that path.") 124 | print("") 125 | self.u.grab("Press [enter] to return...") 126 | continue 127 | # We got what we need - set and return the path 128 | self.output = test_path 129 | return self.output 130 | 131 | def get_ascii_print(self, data): 132 | # Helper to sanitize unprintable characters by replacing them with 133 | # ? where needed 134 | unprintables = False 135 | all_zeroes = True 136 | ascii_string = "" 137 | for b in data: 138 | if not isinstance(b,int): 139 | try: b = ord(b) 140 | except: pass 141 | if b != 0: 142 | # Not wildcard matching 143 | all_zeroes = False 144 | if ord(" ") <= b < ord("~"): 145 | ascii_string += chr(b) 146 | else: 147 | ascii_string += "?" 148 | unprintables = True 149 | return (False if all_zeroes else unprintables,ascii_string) 150 | 151 | def check_normalize(self, patch_or_drop, normalize_headers, check_type="Patch"): 152 | sig = ("OemTableId","TableSignature") 153 | if normalize_headers: 154 | # OpenCore - and NormalizeHeaders is enabled. Check if we have 155 | # any unprintable ASCII chars in our OemTableId or TableSignature 156 | # and warn. 157 | if any(self.get_ascii_print(plist.extract_data(patch_or_drop.get(x,b"\x00")))[0] for x in sig): 158 | print("\n{}!! WARNING !!{} NormalizeHeaders is {}ENABLED{}, and table ids contain unprintable".format( 159 | self.yel, 160 | self.rst, 161 | self.grn, 162 | self.rst 163 | )) 164 | print(" characters! {} may not match or apply!\n".format(check_type)) 165 | return True 166 | else: 167 | # Not enabled - check for question marks as that may imply characters 168 | # were sanitized when creating the patches/dropping tables. 169 | if any(b"\x3F" in plist.extract_data(patch_or_drop.get(x,b"\x00")) for x in sig): 170 | print("\n{}!! WARNING !!{} NormalizeHeaders is {}DISABLED{}, and table ids contain '?'!".format( 171 | self.yel, 172 | self.rst, 173 | self.red, 174 | self.rst 175 | )) 176 | print(" {} may not match or apply!\n".format(check_type)) 177 | return True 178 | return False 179 | 180 | def ensure_path(self, plist_data, path_list, final_type = list): 181 | if not path_list: 182 | return plist_data 183 | if not isinstance(plist_data,dict): 184 | plist_data = {} # Override it with a dict 185 | # Set our initial reference, then iterate the 186 | # path list 187 | last = plist_data 188 | for i,path in enumerate(path_list,start=1): 189 | # Check if our next path var is in last 190 | if not path in last: 191 | last[path] = {} if i < len(path_list) else final_type() 192 | # Make sure it's the correct type if we're at the 193 | # end of the entries 194 | if i >= len(path_list) and not isinstance(last[path],final_type): 195 | # Override it 196 | last[path] = final_type() 197 | # Update our reference 198 | last = last[path] 199 | return plist_data 200 | 201 | def get_unique_name(self,name,target_folder,name_append=""): 202 | # Get a new file name in the target folder so we don't override the original 203 | name = os.path.basename(name) 204 | ext = "" if not "." in name else name.split(".")[-1] 205 | if ext: name = name[:-len(ext)-1] 206 | if name_append: name = name+str(name_append) 207 | check_name = ".".join((name,ext)) if ext else name 208 | if not os.path.exists(os.path.join(target_folder,check_name)): 209 | return check_name 210 | # We need a unique name 211 | num = 1 212 | while True: 213 | check_name = "{}-{}".format(name,num) 214 | if ext: check_name += "."+ext 215 | if not os.path.exists(os.path.join(target_folder,check_name)): 216 | return check_name 217 | num += 1 # Increment our counter 218 | 219 | def pause_interactive(self, return_value=None): 220 | if self.interactive: 221 | print("") 222 | self.u.grab("Press [enter] to return...") 223 | return return_value 224 | 225 | def patch_plist(self): 226 | # Retain the config name 227 | if self.interactive: 228 | self.u.head("Patching Plist") 229 | print("") 230 | # Make sure we have a config_path 231 | if not self.config_path: 232 | print("No target plist path specified!") 233 | return self.pause_interactive() 234 | # Make sure that config_path exists 235 | if not os.path.isfile(self.config_path): 236 | print("Could not locate target plist at:") 237 | print(" - {}".format(self.config_path)) 238 | return self.pause_interactive() 239 | # Make sure our output var has a value 240 | if not self.output: 241 | print("No Results folder path specified!") 242 | return self.pause_interactive() 243 | config_name = os.path.basename(self.config_path) 244 | print("Loading {}...".format(config_name)) 245 | self.config_type,config_data,e = self.get_plist_info(self.config_path) 246 | if e: 247 | print(" - Failed to load! {}".format(e)) 248 | return self.pause_interactive() 249 | # Recheck the config.plist type 250 | if not self.config_type: 251 | print("Could not determine plist type!") 252 | return self.pause_interactive() 253 | # Ensure our patches plists exist, and break out info 254 | # into the target_path and target_name as needed 255 | target_path,_,target_name = self.get_patch_plist_for_type( 256 | self.output, 257 | self.config_type 258 | ) 259 | # This should only show up if output is None/False/empty 260 | if not target_path: 261 | print("Could not locate {} in:".format(target_name or "the required patches plist")) 262 | print(" - {}".format(self.output)) 263 | return self.pause_interactive() 264 | # Make sure the path actually exists - and is a file 265 | if not os.path.isfile(target_path): 266 | print("Could not locate required patches at:") 267 | print(" - {}".format(target_path)) 268 | return self.pause_interactive() 269 | # Set up some preliminary variables for reporting later 270 | errors_found = normalize_headers = False # Default to off 271 | target_name = os.path.basename(target_path) 272 | print("Loading {}...".format(target_name)) 273 | # Load the target plist 274 | _,target_data,e = self.get_plist_info(target_path) 275 | if e: 276 | print(" - Failed to load! {}".format(e)) 277 | return self.pause_interactive() 278 | print("Ensuring paths in {} and {}...".format(config_name,target_name)) 279 | # Make sure all the needed values are there 280 | if self.config_type == "OpenCore": 281 | for p in (("ACPI","Add"),("ACPI","Delete"),("ACPI","Patch")): 282 | print(" - {}...".format(" -> ".join(p))) 283 | config_data = self.ensure_path(config_data,p) 284 | target_data = self.ensure_path(target_data,p) 285 | print(" - ACPI -> Quirks...") 286 | config_data = self.ensure_path(config_data,("ACPI","Quirks"),final_type=dict) 287 | normalize_headers = config_data["ACPI"]["Quirks"].get("NormalizeHeaders",False) 288 | if not isinstance(normalize_headers,(bool)): 289 | errors_found = True 290 | print("\n{}!! WARNING !!{} ACPI -> Quirks -> NormalizeHeaders is malformed - assuming False".format( 291 | self.yel, 292 | self.rst 293 | )) 294 | normalize_headers = False 295 | # Set up our patch sources 296 | ssdts = target_data["ACPI"]["Add"] 297 | patch = target_data["ACPI"]["Patch"] 298 | drops = target_data["ACPI"]["Delete"] 299 | # Set up our original values 300 | s_orig = config_data["ACPI"]["Add"] 301 | p_orig = config_data["ACPI"]["Patch"] 302 | d_orig = config_data["ACPI"]["Delete"] 303 | else: 304 | for p in (("ACPI","DropTables"),("ACPI","SortedOrder"),("ACPI","DSDT","Patches")): 305 | print(" - {}...".format(" -> ".join(p))) 306 | config_data = self.ensure_path(config_data,p) 307 | target_data = self.ensure_path(target_data,p) 308 | # Set up our patch sources 309 | ssdts = target_data["ACPI"]["SortedOrder"] 310 | patch = target_data["ACPI"]["DSDT"]["Patches"] 311 | drops = target_data["ACPI"]["DropTables"] 312 | # Set up our original values 313 | s_orig = config_data["ACPI"]["SortedOrder"] 314 | p_orig = config_data["ACPI"]["DSDT"]["Patches"] 315 | d_orig = config_data["ACPI"]["DropTables"] 316 | print("") 317 | if not ssdts: 318 | print("--- No SSDTs to add - skipping...") 319 | else: 320 | print("--- Walking target SSDTs ({:,} total)...".format(len(ssdts))) 321 | s_rem = [] 322 | # Gather any entries broken from user error 323 | s_broken = [x for x in s_orig if not isinstance(x,dict)] if self.config_type == "OpenCore" else [] 324 | for s in ssdts: 325 | if self.config_type == "OpenCore": 326 | print(" - Checking {}...".format(s["Path"])) 327 | existing = [x for x in s_orig if isinstance(x,dict) and x["Path"] == s["Path"]] 328 | else: 329 | print(" - Checking {}...".format(s)) 330 | existing = [x for x in s_orig if x == s] 331 | if existing: 332 | print(" --> Located {:,} existing to replace...".format(len(existing))) 333 | s_rem.extend(existing) 334 | if s_rem: 335 | print(" - Removing {:,} existing duplicate{}...".format(len(s_rem),"" if len(s_rem)==1 else "s")) 336 | for r in s_rem: 337 | if r in s_orig: s_orig.remove(r) 338 | else: 339 | print(" - No duplicates to remove...") 340 | print(" - Adding {:,} SSDT{}...".format(len(ssdts),"" if len(ssdts)==1 else "s")) 341 | s_orig.extend(ssdts) 342 | if s_broken: 343 | errors_found = True 344 | print("\n{}!! WARNING !!{} {:,} Malformed entr{} found - please fix your {}!".format( 345 | self.yel, 346 | self.rst, 347 | len(s_broken), 348 | "y" if len(d_broken)==1 else "ies", 349 | config_name 350 | )) 351 | print("") 352 | if not patch: 353 | print("--- No patches to add - skipping...") 354 | else: 355 | print("--- Walking target patches ({:,} total)...".format(len(patch))) 356 | p_rem = [] 357 | # Gather any entries broken from user error 358 | p_broken = [x for x in p_orig if not isinstance(x,dict)] 359 | for p in patch: 360 | print(" - Checking {}...".format(p["Comment"])) 361 | if self.config_type == "OpenCore" and self.check_normalize(p,normalize_headers): 362 | errors_found = True 363 | existing = [x for x in p_orig if isinstance(x,dict) and x["Find"] == p["Find"] and x["Replace"] == p["Replace"]] 364 | if existing: 365 | print(" --> Located {:,} existing to replace...".format(len(existing))) 366 | p_rem.extend(existing) 367 | # Remove any dupes 368 | if p_rem: 369 | print(" - Removing {:,} existing duplicate{}...".format(len(p_rem),"" if len(p_rem)==1 else "s")) 370 | for r in p_rem: 371 | if r in p_orig: p_orig.remove(r) 372 | else: 373 | print(" - No duplicates to remove...") 374 | print(" - Adding {:,} patch{}...".format(len(patch),"" if len(patch)==1 else "es")) 375 | p_orig.extend(patch) 376 | if p_broken: 377 | errors_found = True 378 | print("\n{}!! WARNING !!{} {:,} Malformed entr{} found - please fix your {}!".format( 379 | self.yel, 380 | self.rst, 381 | len(p_broken), 382 | "y" if len(d_broken)==1 else "ies", 383 | config_name 384 | )) 385 | print("") 386 | if not drops: 387 | print("--- No tables to drop - skipping...") 388 | else: 389 | print("--- Walking target tables to drop ({:,} total)...".format(len(drops))) 390 | d_rem = [] 391 | # Gather any entries broken from user error 392 | d_broken = [x for x in d_orig if not isinstance(x,dict)] 393 | for d in drops: 394 | if self.config_type == "OpenCore": 395 | print(" - Checking {}...".format(d["Comment"])) 396 | if self.check_normalize(d,normalize_headers,check_type="Dropped table"): 397 | errors_found = True 398 | existing = [x for x in d_orig if isinstance(x,dict) and x["TableSignature"] == d["TableSignature"] and x["OemTableId"] == d["OemTableId"]] 399 | else: 400 | name = " - ".join([x for x in (d.get("Signature",""),d.get("TableId","")) if x]) or "Unknown Dropped Table" 401 | print(" - Checking {}...".format(name)) 402 | existing = [x for x in d_orig if isinstance(x,dict) and x.get("Signature") == d.get("Signature") and x.get("TableId") == d.get("TableId")] 403 | if existing: 404 | print(" --> Located {:,} existing to replace...".format(len(existing))) 405 | d_rem.extend(existing) 406 | if d_rem: 407 | print(" - Removing {:,} existing duplicate{}...".format(len(d_rem),"" if len(d_rem)==1 else "s")) 408 | for r in d_rem: 409 | if r in d_orig: d_orig.remove(r) 410 | else: 411 | print(" - No duplicates to remove...") 412 | print(" - Dropping {:,} table{}...".format(len(drops),"" if len(drops)==1 else "s")) 413 | d_orig.extend(drops) 414 | if d_broken: 415 | errors_found = True 416 | print("\n{}!! WARNING !!{} {:,} Malformed entr{} found - please fix your {}!".format( 417 | self.yel, 418 | self.rst, 419 | len(d_broken), 420 | "y" if len(d_broken)==1 else "ies", 421 | config_name 422 | )) 423 | print("") 424 | if self.overwrite: 425 | output_path = self.config_path 426 | else: 427 | config_name = self.get_unique_name(config_name,self.output) 428 | output_path = os.path.join(self.output,config_name) 429 | print("Saving to {}...".format(output_path)) 430 | try: 431 | plist.dump(config_data,open(output_path,"wb")) 432 | except Exception as e: 433 | print(" - Failed to save! {}".format(e)) 434 | return self.pause_interactive() 435 | print(" - Saved.") 436 | print("") 437 | if errors_found: 438 | print("{}!! WARNING !!{} Potential errors were found when merging - please address them!".format( 439 | self.yel, 440 | self.rst 441 | )) 442 | print("") 443 | if not self.overwrite: 444 | print("{}!! WARNING !!{} Make sure you review the saved {} before replacing!".format( 445 | self.red, 446 | self.rst, 447 | config_name 448 | )) 449 | print("") 450 | print("Done.") 451 | return self.pause_interactive() 452 | 453 | def get_plist_info(self, config_path): 454 | # Attempts to load the passed config and return a tuple 455 | # of (type_string,config_data,error) 456 | type_string = config_data = e = None 457 | try: 458 | config_data = plist.load(open(config_path,"rb")) 459 | except Exception as e: 460 | return (None,None,e) 461 | if not isinstance(config_data,dict): 462 | e = "Invalid root node type: {}".format(type(config_data)) 463 | else: 464 | type_string = "OpenCore" if "PlatformInfo" in config_data else "Clover" if "SMBIOS" in config_data else None 465 | return (type_string,config_data,None) 466 | 467 | def get_patch_plist_for_type(self, path, config_type): 468 | o,c = self._get_patches_plists(path) 469 | return { 470 | "OpenCore":o, 471 | "Clover":c 472 | }.get(config_type,(None,False,None)) 473 | 474 | def select_plist(self): 475 | while True: 476 | self.u.head("Select Plist") 477 | print("") 478 | if self.copy_as_path: 479 | print("NOTE: Currently running as admin on Windows - drag and drop may not work.") 480 | print(" Shift + right-click in Explorer and select 'Copy as path' then paste here instead.") 481 | print("") 482 | print("M. Main Menu") 483 | print("Q. Quit") 484 | print("") 485 | path = self.u.grab("Please drag and drop the config.plist here: ") 486 | if not path: continue 487 | if path.lower() == "m": return 488 | elif path.lower() == "q": self.u.custom_quit() 489 | test_path = self.u.check_path(path) 490 | if not test_path or not os.path.isfile(test_path): 491 | self.u.head("Invalid Path") 492 | print("") 493 | print("That path either does not exist, or is not a file.") 494 | print("") 495 | self.u.grab("Press [enter] to return...") 496 | continue 497 | # Got a file - try to load it 498 | t,_,e = self.get_plist_info(test_path) 499 | if e: 500 | self.u.head("Invalid File") 501 | print("") 502 | print("That file failed to load:\n\n{}".format(e)) 503 | print("") 504 | self.u.grab("Press [enter] to return...") 505 | continue 506 | # Got a valid file 507 | self.config_path = test_path 508 | self.config_type = t 509 | return 510 | 511 | def main(self): 512 | # Gather some preliminary info for display 513 | target_path,target_exists,target_name = self.get_patch_plist_for_type( 514 | self.output, 515 | self.config_type 516 | ) 517 | self.u.resize(self.w,self.h) 518 | self.u.head() 519 | print("") 520 | print("Current config.plist: {}".format(self.config_path)) 521 | print("Type of config.plist: {}".format(self.config_type or "Unknown")) 522 | print("Results Folder: {}".format(self.output)) 523 | print("Patches Plist: {}{}".format( 524 | target_name or "Unknown", 525 | "" if (not target_name or target_exists) else " - {}!! MISSING !!{}".format(self.red,self.rst) 526 | )) 527 | print("Overwrite Original: {}{}{}{}".format( 528 | self.red if self.overwrite else self.grn, 529 | "!! True !!" if self.overwrite else "False", 530 | self.rst, 531 | " - Make Sure You Have A Backup!" if self.overwrite else "" 532 | )) 533 | print("") 534 | print("C. Select config.plist") 535 | print("O. Toggle Overwrite Original") 536 | print("R. Select Results Folder") 537 | if self.config_path and target_exists: 538 | print("P. Patch with {}".format(target_name)) 539 | print("") 540 | print("Q. Quit") 541 | print("") 542 | menu = self.u.grab("Please make a selection: ") 543 | if not len(menu): 544 | return 545 | if menu.lower() == "q": 546 | self.u.custom_quit() 547 | elif menu.lower() == "c": 548 | self.select_plist() 549 | elif menu.lower() == "o": 550 | self.overwrite ^= True 551 | elif menu.lower() == "r": 552 | self.select_results_folder() 553 | elif menu.lower() == "p" and self.config_path and target_exists: 554 | self.patch_plist() 555 | 556 | if __name__ == '__main__': 557 | # Setup the cli args 558 | parser = argparse.ArgumentParser(prog="PatchMerge.py", description="PatchMerge - py script to merge patches_[OC/Clover].plist with a config.plist.") 559 | parser.add_argument("-c", "--config", help="path to target config.plist - required if running in non-interactive mode") 560 | parser.add_argument("-r", "--results", help="path to Results folder containing patches_[OC/Clover].plist - required if running in non-interactive mode") 561 | parser.add_argument("-o", "--overwrite", help="overwrite the original config.plist", action="store_true") 562 | parser.add_argument("-i", "--no-interaction", help="run in non-interactive mode - requires -c and -r", action="store_true") 563 | 564 | args = parser.parse_args() 565 | 566 | p = PatchMerge( 567 | config=args.config, 568 | results=args.results, 569 | overwrite=args.overwrite, 570 | interactive=not args.no_interaction 571 | ) 572 | 573 | if args.no_interaction: 574 | # We're in non-interactive mode here 575 | p.patch_plist() 576 | else: 577 | # Interactive mode 578 | if 2/3 == 0: 579 | input = raw_input 580 | while True: 581 | try: 582 | p.main() 583 | except Exception as e: 584 | print("An error occurred: {}".format(e)) 585 | print("") 586 | input("Press [enter] to continue...") 587 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SSDTTime 2 | ========== 3 | A simple tool designed to make creating SSDTs simple. 4 | Supports macOS, Linux and Windows 5 | 6 | ## Supported SSDTs: 7 | - SSDT-HPET 8 | - Patches out IRQ conflicts 9 | - SSDT-EC 10 | - OS-aware fake EC (laptop and desktop variants) 11 | - SSDT-USBX 12 | - Provides generic USB power properties 13 | - SSDT-PLUG 14 | - Sets plugin-type = 1 on CPU0/PR00 15 | - SSDT-PMC 16 | - Adds missing PMCR device for native 300-series NVRAM 17 | - SSDT-AWAC 18 | - Disables AWAC clock, and enables (or fakes) RTC as needed 19 | - SSDT-USB-Reset 20 | - Returns a zero status for detected root hubs to allow hardware querying 21 | - SSDT-Bridge 22 | - Create missing PCI bridges for passed device path 23 | - SSDT-PNLF 24 | - Sets up a PNLF device for laptop backlight control 25 | - SSDT-XOSI 26 | - _OSI rename and patch to return true for a range of Windows versions - also checks for OSID 27 | - DMAR 28 | - Remove Reserved Memory Regions from the DMAR table 29 | - SSDT-SBUS-MCHC 30 | - Defines an MCHC and BUS0 device for SMBus compatibility 31 | - IMEI Bridge 32 | - Defines IMEI - only needed on SNB + 7-series or IVB + 6-series 33 | 34 | Additionally on Linux and Windows the tool can be used to dump the system DSDT. 35 | 36 | ## Instructions: 37 | ### Linux: 38 | * Launch SSDTTime.py with any somewhat recent version of Python from either a terminal window or by running the file normally. 39 | ### macOS: 40 | * Launch SSDTTime.command from either a terminal window or by double clicking the file. 41 | ### Windows: 42 | * Launch SSDTTime.bat from either a terminal window or by double clicking the file. 43 | 44 | ## Credits: 45 | - [CorpNewt](https://github.com/CorpNewt) - Writing the script and libraries used 46 | - [NoOne](https://github.com/IOIIIO) - Some small improvements to the script 47 | - Rehabman/Intel - iasl 48 | -------------------------------------------------------------------------------- /SSDTTime.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Get our local path and args before delayed expansion - allows % and ! 3 | set "thisDir=%~dp0" 4 | set "args=%*" 5 | 6 | setlocal enableDelayedExpansion 7 | REM Setup initial vars 8 | set "script_name=" 9 | set /a tried=0 10 | set "toask=yes" 11 | set "pause_on_error=yes" 12 | set "py2v=" 13 | set "py2path=" 14 | set "py3v=" 15 | set "py3path=" 16 | set "pypath=" 17 | set "targetpy=3" 18 | 19 | REM use_py3: 20 | REM TRUE = Use if found, use py2 otherwise 21 | REM FALSE = Use py2 22 | REM FORCE = Use py3 23 | set "use_py3=TRUE" 24 | 25 | REM We'll parse if the first argument passed is 26 | REM --install-python and if so, we'll just install 27 | REM Can optionally take a version number as the 28 | REM second arg - i.e. --install-python 3.13.1 29 | set "just_installing=FALSE" 30 | set "user_provided=" 31 | 32 | REM Get the system32 (or equivalent) path 33 | call :getsyspath "syspath" 34 | 35 | REM Make sure the syspath exists 36 | if "!syspath!" == "" ( 37 | if exist "%SYSTEMROOT%\system32\cmd.exe" ( 38 | if exist "%SYSTEMROOT%\system32\reg.exe" ( 39 | if exist "%SYSTEMROOT%\system32\where.exe" ( 40 | REM Fall back on the default path if it exists 41 | set "ComSpec=%SYSTEMROOT%\system32\cmd.exe" 42 | set "syspath=%SYSTEMROOT%\system32\" 43 | ) 44 | ) 45 | ) 46 | if "!syspath!" == "" ( 47 | cls 48 | echo ### ### 49 | echo # Missing Required Files # 50 | echo ### ### 51 | echo. 52 | echo Could not locate cmd.exe, reg.exe, or where.exe 53 | echo. 54 | echo Please ensure your ComSpec environment variable is properly configured and 55 | echo points directly to cmd.exe, then try again. 56 | echo. 57 | echo Current CompSpec Value: "%ComSpec%" 58 | echo. 59 | echo Press [enter] to quit. 60 | pause > nul 61 | exit /b 1 62 | ) 63 | ) 64 | 65 | if "%~1" == "--install-python" ( 66 | set "just_installing=TRUE" 67 | set "user_provided=%~2" 68 | goto installpy 69 | ) 70 | 71 | goto checkscript 72 | 73 | :checkscript 74 | REM Check for our script first 75 | set "looking_for=!script_name!" 76 | if "!script_name!" == "" ( 77 | set "looking_for=%~n0.py or %~n0.command" 78 | set "script_name=%~n0.py" 79 | if not exist "!thisDir!\!script_name!" ( 80 | set "script_name=%~n0.command" 81 | ) 82 | ) 83 | if not exist "!thisDir!\!script_name!" ( 84 | cls 85 | echo ### ### 86 | echo # Target Not Found # 87 | echo ### ### 88 | echo. 89 | echo Could not find !looking_for!. 90 | echo Please make sure to run this script from the same directory 91 | echo as !looking_for!. 92 | echo. 93 | echo Press [enter] to quit. 94 | pause > nul 95 | exit /b 1 96 | ) 97 | goto checkpy 98 | 99 | :checkpy 100 | call :updatepath 101 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 102 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 103 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher "%%x" "py2v" "py2path" "py3v" "py3path" ) 104 | REM Walk our returns to see if we need to install 105 | if /i "!use_py3!" == "FALSE" ( 106 | set "targetpy=2" 107 | set "pypath=!py2path!" 108 | ) else if /i "!use_py3!" == "FORCE" ( 109 | set "pypath=!py3path!" 110 | ) else if /i "!use_py3!" == "TRUE" ( 111 | set "pypath=!py3path!" 112 | if "!pypath!" == "" set "pypath=!py2path!" 113 | ) 114 | if not "!pypath!" == "" ( 115 | goto runscript 116 | ) 117 | if !tried! lss 1 ( 118 | if /i "!toask!"=="yes" ( 119 | REM Better ask permission first 120 | goto askinstall 121 | ) else ( 122 | goto installpy 123 | ) 124 | ) else ( 125 | cls 126 | echo ### ### 127 | echo # Python Not Found # 128 | echo ### ### 129 | echo. 130 | REM Couldn't install for whatever reason - give the error message 131 | echo Python is not installed or not found in your PATH var. 132 | echo Please go to https://www.python.org/downloads/windows/ to 133 | echo download and install the latest version, then try again. 134 | echo. 135 | echo Make sure you check the box labeled: 136 | echo. 137 | echo "Add Python X.X to PATH" 138 | echo. 139 | echo Where X.X is the py version you're installing. 140 | echo. 141 | echo Press [enter] to quit. 142 | pause > nul 143 | exit /b 1 144 | ) 145 | goto runscript 146 | 147 | :checkpylauncher 148 | REM Attempt to check the latest python 2 and 3 versions via the py launcher 149 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -2 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 150 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -3 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 151 | goto :EOF 152 | 153 | :checkpyversion 154 | set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) do ( 155 | REM Ensure we have a version number 156 | call :isnumber "%%a" 157 | if not "!errorlevel!" == "0" goto :EOF 158 | set "version=%%a" 159 | ) 160 | if not defined version goto :EOF 161 | if "!version:~0,1!" == "2" ( 162 | REM Python 2 163 | call :comparepyversion "!version!" "!%~2!" 164 | if "!errorlevel!" == "1" ( 165 | set "%~2=!version!" 166 | set "%~3=%~1" 167 | ) 168 | ) else ( 169 | REM Python 3 170 | call :comparepyversion "!version!" "!%~4!" 171 | if "!errorlevel!" == "1" ( 172 | set "%~4=!version!" 173 | set "%~5=%~1" 174 | ) 175 | ) 176 | goto :EOF 177 | 178 | :isnumber 179 | set "var="&for /f "delims=0123456789." %%i in ("%~1") do set var=%%i 180 | if defined var (exit /b 1) 181 | exit /b 0 182 | 183 | :comparepyversion 184 | REM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2 185 | for /f "tokens=1,2,3 delims=." %%a in ("%~1") do ( 186 | set a1=%%a 187 | set a2=%%b 188 | set a3=%%c 189 | ) 190 | for /f "tokens=1,2,3 delims=." %%a in ("%~2") do ( 191 | set b1=%%a 192 | set b2=%%b 193 | set b3=%%c 194 | ) 195 | if not defined a1 set a1=0 196 | if not defined a2 set a2=0 197 | if not defined a3 set a3=0 198 | if not defined b1 set b1=0 199 | if not defined b2 set b2=0 200 | if not defined b3 set b3=0 201 | if %a1% gtr %b1% exit /b 1 202 | if %a1% lss %b1% exit /b 2 203 | if %a2% gtr %b2% exit /b 1 204 | if %a2% lss %b2% exit /b 2 205 | if %a3% gtr %b3% exit /b 1 206 | if %a3% lss %b3% exit /b 2 207 | exit /b 0 208 | 209 | :askinstall 210 | cls 211 | echo ### ### 212 | echo # Python Not Found # 213 | echo ### ### 214 | echo. 215 | echo Python !targetpy! was not found on the system or in the PATH var. 216 | echo. 217 | set /p "menu=Would you like to install it now? [y/n]: " 218 | if /i "!menu!"=="y" ( 219 | REM We got the OK - install it 220 | goto installpy 221 | ) else if "!menu!"=="n" ( 222 | REM No OK here... 223 | set /a tried=!tried!+1 224 | goto checkpy 225 | ) 226 | REM Incorrect answer - go back 227 | goto askinstall 228 | 229 | :installpy 230 | REM This will attempt to download and install python 231 | set /a tried=!tried!+1 232 | cls 233 | echo ### ### 234 | echo # Downloading Python # 235 | echo ### ### 236 | echo. 237 | set "release=!user_provided!" 238 | if "!release!" == "" ( 239 | REM No explicit release set - get the latest from python.org 240 | echo Gathering latest version... 241 | powershell -command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;(new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\pyurl.txt')" 242 | REM Extract it if it's gzip compressed 243 | powershell -command "$infile='%TEMP%\pyurl.txt';$outfile='%TEMP%\pyurl.temp';try{$input=New-Object System.IO.FileStream $infile,([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read);$output=New-Object System.IO.FileStream $outfile,([IO.FileMode]::Create),([IO.FileAccess]::Write),([IO.FileShare]::None);$gzipStream=New-Object System.IO.Compression.GzipStream $input,([IO.Compression.CompressionMode]::Decompress);$buffer=New-Object byte[](1024);while($true){$read=$gzipstream.Read($buffer,0,1024);if($read -le 0){break};$output.Write($buffer,0,$read)};$gzipStream.Close();$output.Close();$input.Close();Move-Item -Path $outfile -Destination $infile -Force}catch{}" 244 | if not exist "%TEMP%\pyurl.txt" ( 245 | if /i "!just_installing!" == "TRUE" ( 246 | echo - Failed to get info 247 | exit /b 1 248 | ) else ( 249 | goto checkpy 250 | ) 251 | ) 252 | pushd "%TEMP%" 253 | :: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20) 254 | for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python !targetpy! Release" pyurl.txt') do ( set "release=%%x" ) 255 | popd 256 | REM Let's delete our txt file now - we no longer need it 257 | del "%TEMP%\pyurl.txt" 258 | if "!release!" == "" ( 259 | if /i "!just_installing!" == "TRUE" ( 260 | echo - Failed to get python version 261 | exit /b 1 262 | ) else ( 263 | goto checkpy 264 | ) 265 | ) 266 | echo Located Version: !release! 267 | ) else ( 268 | echo User-Provided Version: !release! 269 | REM Update our targetpy to reflect the first number of 270 | REM our release 271 | for /f "tokens=1 delims=." %%a in ("!release!") do ( 272 | call :isnumber "%%a" 273 | if "!errorlevel!" == "0" ( 274 | set "targetpy=%%a" 275 | ) 276 | ) 277 | ) 278 | echo Building download url... 279 | REM At this point - we should have the version number. 280 | REM We can build the url like so: "https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe" 281 | set "url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe" 282 | set "pytype=exe" 283 | if "!targetpy!" == "2" ( 284 | set "url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi" 285 | set "pytype=msi" 286 | ) 287 | echo - !url! 288 | echo Downloading... 289 | REM Now we download it with our slick powershell command 290 | powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\pyinstall.!pytype!')" 291 | REM If it doesn't exist - we bail 292 | if not exist "%TEMP%\pyinstall.!pytype!" ( 293 | if /i "!just_installing!" == "TRUE" ( 294 | echo - Failed to download python installer 295 | exit /b 1 296 | ) else ( 297 | goto checkpy 298 | ) 299 | ) 300 | REM It should exist at this point - let's run it to install silently 301 | echo Running python !pytype! installer... 302 | pushd "%TEMP%" 303 | if /i "!pytype!" == "exe" ( 304 | echo - pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 305 | pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 306 | ) else ( 307 | set "foldername=!release:.=!" 308 | echo - msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 309 | msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 310 | ) 311 | popd 312 | set "py_error=!errorlevel!" 313 | echo Installer finished with status: !py_error! 314 | echo Cleaning up... 315 | REM Now we should be able to delete the installer and check for py again 316 | del "%TEMP%\pyinstall.!pytype!" 317 | REM If it worked, then we should have python in our PATH 318 | REM this does not get updated right away though - let's try 319 | REM manually updating the local PATH var 320 | call :updatepath 321 | if /i "!just_installing!" == "TRUE" ( 322 | echo. 323 | echo Done. 324 | ) else ( 325 | goto checkpy 326 | ) 327 | exit /b 328 | 329 | :runscript 330 | REM Python found 331 | cls 332 | REM Checks the args gathered at the beginning of the script. 333 | REM Make sure we're not just forwarding empty quotes. 334 | set "arg_test=!args:"=!" 335 | if "!arg_test!"=="" ( 336 | "!pypath!" "!thisDir!!script_name!" 337 | ) else ( 338 | "!pypath!" "!thisDir!!script_name!" !args! 339 | ) 340 | if /i "!pause_on_error!" == "yes" ( 341 | if not "%ERRORLEVEL%" == "0" ( 342 | echo. 343 | echo Script exited with error code: %ERRORLEVEL% 344 | echo. 345 | echo Press [enter] to exit... 346 | pause > nul 347 | ) 348 | ) 349 | goto :EOF 350 | 351 | :undouble 352 | REM Helper function to strip doubles of a single character out of a string recursively 353 | set "string_value=%~2" 354 | :undouble_continue 355 | set "check=!string_value:%~3%~3=%~3!" 356 | if not "!check!" == "!string_value!" ( 357 | set "string_value=!check!" 358 | goto :undouble_continue 359 | ) 360 | set "%~1=!check!" 361 | goto :EOF 362 | 363 | :updatepath 364 | set "spath=" 365 | set "upath=" 366 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKCU\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "upath=%%j" ) 367 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "spath=%%j" ) 368 | if not "%spath%" == "" ( 369 | REM We got something in the system path 370 | set "PATH=%spath%" 371 | if not "%upath%" == "" ( 372 | REM We also have something in the user path 373 | set "PATH=%PATH%;%upath%" 374 | ) 375 | ) else if not "%upath%" == "" ( 376 | set "PATH=%upath%" 377 | ) 378 | REM Remove double semicolons from the adjusted PATH 379 | call :undouble "PATH" "%PATH%" ";" 380 | goto :EOF 381 | 382 | :getsyspath 383 | REM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe by 384 | REM walking the ComSpec var - will also repair it in memory if need be 385 | REM Strip double semi-colons 386 | call :undouble "temppath" "%ComSpec%" ";" 387 | 388 | REM Dirty hack to leverage the "line feed" approach - there are some odd side 389 | REM effects with this. Do not use this variable name in comments near this 390 | REM line - as it seems to behave erradically. 391 | (set LF=^ 392 | %=this line is empty=% 393 | ) 394 | REM Replace instances of semi-colons with a line feed and wrap 395 | REM in parenthesis to work around some strange batch behavior 396 | set "testpath=%temppath:;=!LF!%" 397 | 398 | REM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there 399 | set /a found=0 400 | for /f "tokens=* delims=" %%i in ("!testpath!") do ( 401 | REM Only continue if we haven't found it yet 402 | if not "%%i" == "" ( 403 | if !found! lss 1 ( 404 | set "checkpath=%%i" 405 | REM Remove "cmd.exe" from the end if it exists 406 | if /i "!checkpath:~-7!" == "cmd.exe" ( 407 | set "checkpath=!checkpath:~0,-7!" 408 | ) 409 | REM Pad the end with a backslash if needed 410 | if not "!checkpath:~-1!" == "\" ( 411 | set "checkpath=!checkpath!\" 412 | ) 413 | REM Let's see if cmd, reg, and where exist there - and set it if so 414 | if EXIST "!checkpath!cmd.exe" ( 415 | if EXIST "!checkpath!reg.exe" ( 416 | if EXIST "!checkpath!where.exe" ( 417 | set /a found=1 418 | set "ComSpec=!checkpath!cmd.exe" 419 | set "%~1=!checkpath!" 420 | ) 421 | ) 422 | ) 423 | ) 424 | ) 425 | ) 426 | goto :EOF 427 | -------------------------------------------------------------------------------- /SSDTTime.command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the curent directory, the script name 4 | # and the script name with "py" substituted for the extension. 5 | args=( "$@" ) 6 | dir="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" 7 | script="${0##*/}" 8 | target="${script%.*}.py" 9 | 10 | # use_py3: 11 | # TRUE = Use if found, use py2 otherwise 12 | # FALSE = Use py2 13 | # FORCE = Use py3 14 | use_py3="TRUE" 15 | 16 | # We'll parse if the first argument passed is 17 | # --install-python and if so, we'll just install 18 | # Can optionally take a version number as the 19 | # second arg - i.e. --install-python 3.13.1 20 | just_installing="FALSE" 21 | 22 | tempdir="" 23 | 24 | compare_to_version () { 25 | # Compares our OS version to the passed OS version, and 26 | # return a 1 if we match the passed compare type, or a 0 if we don't. 27 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 28 | # $2 = OS version to compare ours to 29 | if [ -z "$1" ] || [ -z "$2" ]; then 30 | # Missing info - bail. 31 | return 32 | fi 33 | local current_os= comp= 34 | current_os="$(sw_vers -productVersion 2>/dev/null)" 35 | comp="$(vercomp "$current_os" "$2")" 36 | # Check gequal and lequal first 37 | if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || [[ "$comp" == "$1" ]]; then 38 | # Matched 39 | echo "1" 40 | else 41 | # No match 42 | echo "0" 43 | fi 44 | } 45 | 46 | set_use_py3_if () { 47 | # Auto sets the "use_py3" variable based on 48 | # conditions passed 49 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 50 | # $2 = OS version to compare 51 | # $3 = TRUE/FALSE/FORCE in case of match 52 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 53 | # Missing vars - bail with no changes. 54 | return 55 | fi 56 | if [ "$(compare_to_version "$1" "$2")" == "1" ]; then 57 | use_py3="$3" 58 | fi 59 | } 60 | 61 | get_remote_py_version () { 62 | local pyurl= py_html= py_vers= py_num="3" 63 | pyurl="https://www.python.org/downloads/macos/" 64 | py_html="$(curl -L $pyurl --compressed 2>&1)" 65 | if [ -z "$use_py3" ]; then 66 | use_py3="TRUE" 67 | fi 68 | if [ "$use_py3" == "FALSE" ]; then 69 | py_num="2" 70 | fi 71 | py_vers="$(echo "$py_html" | grep -i "Latest Python $py_num Release" | awk '{print $8}' | cut -d'<' -f1)" 72 | echo "$py_vers" 73 | } 74 | 75 | download_py () { 76 | local vers="$1" url= 77 | clear 78 | echo " ### ###" 79 | echo " # Downloading Python #" 80 | echo "### ###" 81 | echo 82 | if [ -z "$vers" ]; then 83 | echo "Gathering latest version..." 84 | vers="$(get_remote_py_version)" 85 | if [ -z "$vers" ]; then 86 | if [ "$just_installing" == "TRUE" ]; then 87 | echo " - Failed to get info!" 88 | exit 1 89 | else 90 | # Didn't get it still - bail 91 | print_error 92 | fi 93 | fi 94 | echo "Located Version: $vers" 95 | else 96 | # Got a version passed 97 | echo "User-Provided Version: $vers" 98 | fi 99 | echo "Building download url..." 100 | url="$(curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }' | head -n 1)" 101 | if [ -z "$url" ]; then 102 | if [ "$just_installing" == "TRUE" ]; then 103 | echo " - Failed to build download url!" 104 | exit 1 105 | else 106 | # Couldn't get the URL - bail 107 | print_error 108 | fi 109 | fi 110 | echo " - $url" 111 | echo "Downloading..." 112 | # Create a temp dir and download to it 113 | tempdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')" 114 | curl "$url" -o "$tempdir/python.pkg" 115 | if [ "$?" != "0" ]; then 116 | echo " - Failed to download python installer!" 117 | exit $? 118 | fi 119 | echo 120 | echo "Running python install package..." 121 | echo 122 | sudo installer -pkg "$tempdir/python.pkg" -target / 123 | echo 124 | if [ "$?" != "0" ]; then 125 | echo " - Failed to install python!" 126 | exit $? 127 | fi 128 | # Now we expand the package and look for a shell update script 129 | pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" 130 | if [ -e "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" ]; then 131 | # Run the script 132 | echo "Updating PATH..." 133 | echo 134 | "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" 135 | echo 136 | fi 137 | vers_folder="Python $(echo "$vers" | cut -d'.' -f1 -f2)" 138 | if [ -f "/Applications/$vers_folder/Install Certificates.command" ]; then 139 | # Certs script exists - let's execute that to make sure our certificates are updated 140 | echo "Updating Certificates..." 141 | echo 142 | "/Applications/$vers_folder/Install Certificates.command" 143 | echo 144 | fi 145 | echo "Cleaning up..." 146 | cleanup 147 | if [ "$just_installing" == "TRUE" ]; then 148 | echo 149 | echo "Done." 150 | else 151 | # Now we check for py again 152 | downloaded="TRUE" 153 | clear 154 | main 155 | fi 156 | } 157 | 158 | cleanup () { 159 | if [ -d "$tempdir" ]; then 160 | rm -Rf "$tempdir" 161 | fi 162 | } 163 | 164 | print_error() { 165 | clear 166 | cleanup 167 | echo " ### ###" 168 | echo " # Python Not Found #" 169 | echo "### ###" 170 | echo 171 | echo "Python is not installed or not found in your PATH var." 172 | echo 173 | if [ "$kernel" == "Darwin" ]; then 174 | echo "Please go to https://www.python.org/downloads/macos/ to" 175 | echo "download and install the latest version, then try again." 176 | else 177 | echo "Please install python through your package manager and" 178 | echo "try again." 179 | fi 180 | echo 181 | exit 1 182 | } 183 | 184 | print_target_missing() { 185 | clear 186 | cleanup 187 | echo " ### ###" 188 | echo " # Target Not Found #" 189 | echo "### ###" 190 | echo 191 | echo "Could not locate $target!" 192 | echo 193 | exit 1 194 | } 195 | 196 | format_version () { 197 | local vers="$1" 198 | echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" 199 | } 200 | 201 | vercomp () { 202 | # Modified from: https://apple.stackexchange.com/a/123408/11374 203 | local ver1="$(format_version "$1")" ver2="$(format_version "$2")" 204 | if [ $ver1 -gt $ver2 ]; then 205 | echo "1" 206 | elif [ $ver1 -lt $ver2 ]; then 207 | echo "2" 208 | else 209 | echo "0" 210 | fi 211 | } 212 | 213 | get_local_python_version() { 214 | # $1 = Python bin name (defaults to python3) 215 | # Echoes the path to the highest version of the passed python bin if any 216 | local py_name="$1" max_version= python= python_version= python_path= 217 | if [ -z "$py_name" ]; then 218 | py_name="python3" 219 | fi 220 | py_list="$(which -a "$py_name" 2>/dev/null)" 221 | # Walk that newline separated list 222 | while read python; do 223 | if [ -z "$python" ]; then 224 | # Got a blank line - skip 225 | continue 226 | fi 227 | if [ "$check_py3_stub" == "1" ] && [ "$python" == "/usr/bin/python3" ]; then 228 | # See if we have a valid developer path 229 | xcode-select -p > /dev/null 2>&1 230 | if [ "$?" != "0" ]; then 231 | # /usr/bin/python3 path - but no valid developer dir 232 | continue 233 | fi 234 | fi 235 | python_version="$(get_python_version $python)" 236 | if [ -z "$python_version" ]; then 237 | # Didn't find a py version - skip 238 | continue 239 | fi 240 | # Got the py version - compare to our max 241 | if [ -z "$max_version" ] || [ "$(vercomp "$python_version" "$max_version")" == "1" ]; then 242 | # Max not set, or less than the current - update it 243 | max_version="$python_version" 244 | python_path="$python" 245 | fi 246 | done <<< "$py_list" 247 | echo "$python_path" 248 | } 249 | 250 | get_python_version() { 251 | local py_path="$1" py_version= 252 | # Get the python version by piping stderr into stdout (for py2), then grepping the output for 253 | # the word "python", getting the second element, and grepping for an alphanumeric version number 254 | py_version="$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[A-Za-z\d\.]+")" 255 | if [ ! -z "$py_version" ]; then 256 | echo "$py_version" 257 | fi 258 | } 259 | 260 | prompt_and_download() { 261 | if [ "$downloaded" != "FALSE" ] || [ "$kernel" != "Darwin" ]; then 262 | # We already tried to download, or we're not on macOS - just bail 263 | print_error 264 | fi 265 | clear 266 | echo " ### ###" 267 | echo " # Python Not Found #" 268 | echo "### ###" 269 | echo 270 | target_py="Python 3" 271 | printed_py="Python 2 or 3" 272 | if [ "$use_py3" == "FORCE" ]; then 273 | printed_py="Python 3" 274 | elif [ "$use_py3" == "FALSE" ]; then 275 | target_py="Python 2" 276 | printed_py="Python 2" 277 | fi 278 | echo "Could not locate $printed_py!" 279 | echo 280 | echo "This script requires $printed_py to run." 281 | echo 282 | while true; do 283 | read -p "Would you like to install the latest $target_py now? (y/n): " yn 284 | case $yn in 285 | [Yy]* ) download_py;break;; 286 | [Nn]* ) print_error;; 287 | esac 288 | done 289 | } 290 | 291 | main() { 292 | local python= version= 293 | # Verify our target exists 294 | if [ ! -f "$dir/$target" ]; then 295 | # Doesn't exist 296 | print_target_missing 297 | fi 298 | if [ -z "$use_py3" ]; then 299 | use_py3="TRUE" 300 | fi 301 | if [ "$use_py3" != "FALSE" ]; then 302 | # Check for py3 first 303 | python="$(get_local_python_version python3)" 304 | fi 305 | if [ "$use_py3" != "FORCE" ] && [ -z "$python" ]; then 306 | # We aren't using py3 explicitly, and we don't already have a path 307 | python="$(get_local_python_version python2)" 308 | if [ -z "$python" ]; then 309 | # Try just looking for "python" 310 | python="$(get_local_python_version python)" 311 | fi 312 | fi 313 | if [ -z "$python" ]; then 314 | # Didn't ever find it - prompt 315 | prompt_and_download 316 | return 1 317 | fi 318 | # Found it - start our script and pass all args 319 | "$python" "$dir/$target" "${args[@]}" 320 | } 321 | 322 | # Keep track of whether or not we're on macOS to determine if 323 | # we can download and install python for the user as needed. 324 | kernel="$(uname -s)" 325 | # Check to see if we need to force based on 326 | # macOS version. 10.15 has a dummy python3 version 327 | # that can trip up some py3 detection in other scripts. 328 | # set_use_py3_if "3" "10.15" "FORCE" 329 | downloaded="FALSE" 330 | # Check for the aforementioned /usr/bin/python3 stub if 331 | # our OS version is 10.15 or greater. 332 | check_py3_stub="$(compare_to_version "3" "10.15")" 333 | trap cleanup EXIT 334 | if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then 335 | just_installing="TRUE" 336 | download_py "$2" 337 | else 338 | main 339 | fi 340 | -------------------------------------------------------------------------------- /Scripts/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename, isfile 2 | import glob 3 | modules = glob.glob(dirname(__file__)+"/*.py") 4 | __all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] -------------------------------------------------------------------------------- /Scripts/downloader.py: -------------------------------------------------------------------------------- 1 | import sys, os, time, ssl, gzip, multiprocessing 2 | from io import BytesIO 3 | # Python-aware urllib stuff 4 | try: 5 | from urllib.request import urlopen, Request 6 | import queue as q 7 | except ImportError: 8 | # Import urllib2 to catch errors 9 | import urllib2 10 | from urllib2 import urlopen, Request 11 | import Queue as q 12 | 13 | TERMINAL_WIDTH = 120 if os.name=="nt" else 80 14 | 15 | def get_size(size, suffix=None, use_1024=False, round_to=2, strip_zeroes=False): 16 | # size is the number of bytes 17 | # suffix is the target suffix to locate (B, KB, MB, etc) - if found 18 | # use_2014 denotes whether or not we display in MiB vs MB 19 | # round_to is the number of dedimal points to round our result to (0-15) 20 | # strip_zeroes denotes whether we strip out zeroes 21 | 22 | # Failsafe in case our size is unknown 23 | if size == -1: 24 | return "Unknown" 25 | # Get our suffixes based on use_1024 26 | ext = ["B","KiB","MiB","GiB","TiB","PiB"] if use_1024 else ["B","KB","MB","GB","TB","PB"] 27 | div = 1024 if use_1024 else 1000 28 | s = float(size) 29 | s_dict = {} # Initialize our dict 30 | # Iterate the ext list, and divide by 1000 or 1024 each time to setup the dict {ext:val} 31 | for e in ext: 32 | s_dict[e] = s 33 | s /= div 34 | # Get our suffix if provided - will be set to None if not found, or if started as None 35 | suffix = next((x for x in ext if x.lower() == suffix.lower()),None) if suffix else suffix 36 | # Get the largest value that's still over 1 37 | biggest = suffix if suffix else next((x for x in ext[::-1] if s_dict[x] >= 1), "B") 38 | # Determine our rounding approach - first make sure it's an int; default to 2 on error 39 | try:round_to=int(round_to) 40 | except:round_to=2 41 | round_to = 0 if round_to < 0 else 15 if round_to > 15 else round_to # Ensure it's between 0 and 15 42 | bval = round(s_dict[biggest], round_to) 43 | # Split our number based on decimal points 44 | a,b = str(bval).split(".") 45 | # Check if we need to strip or pad zeroes 46 | b = b.rstrip("0") if strip_zeroes else b.ljust(round_to,"0") if round_to > 0 else "" 47 | return "{:,}{} {}".format(int(a),"" if not b else "."+b,biggest) 48 | 49 | def _process_hook(queue, total_size, bytes_so_far=0, update_interval=1.0, max_packets=0): 50 | packets = [] 51 | speed = remaining = "" 52 | last_update = time.time() 53 | while True: 54 | # Write our info first so we have *some* status while 55 | # waiting for packets 56 | if total_size > 0: 57 | percent = float(bytes_so_far) / total_size 58 | percent = round(percent*100, 2) 59 | t_s = get_size(total_size) 60 | try: 61 | b_s = get_size(bytes_so_far, t_s.split(" ")[1]) 62 | except: 63 | b_s = get_size(bytes_so_far) 64 | perc_str = " {:.2f}%".format(percent) 65 | bar_width = (TERMINAL_WIDTH // 3)-len(perc_str) 66 | progress = "=" * int(bar_width * (percent/100)) 67 | sys.stdout.write("\r\033[K{}/{} | {}{}{}{}{}".format( 68 | b_s, 69 | t_s, 70 | progress, 71 | " " * (bar_width-len(progress)), 72 | perc_str, 73 | speed, 74 | remaining 75 | )) 76 | else: 77 | b_s = get_size(bytes_so_far) 78 | sys.stdout.write("\r\033[K{}{}".format(b_s, speed)) 79 | sys.stdout.flush() 80 | # Now we gather the next packet 81 | try: 82 | packet = queue.get(timeout=update_interval) 83 | # Packets should be formatted as a tuple of 84 | # (timestamp, len(bytes_downloaded)) 85 | # If "DONE" is passed, we assume the download 86 | # finished - and bail 87 | if packet == "DONE": 88 | print("") # Jump to the next line 89 | return 90 | # Append our packet to the list and ensure we're not 91 | # beyond our max. 92 | # Only check max if it's > 0 93 | packets.append(packet) 94 | if max_packets > 0: 95 | packets = packets[-max_packets:] 96 | # Increment our bytes so far as well 97 | bytes_so_far += packet[1] 98 | except q.Empty: 99 | # Didn't get anything - reset the speed 100 | # and packets 101 | packets = [] 102 | speed = " | 0 B/s" 103 | remaining = " | ?? left" if total_size > 0 else "" 104 | except KeyboardInterrupt: 105 | print("") # Jump to the next line 106 | return 107 | # If we have packets and it's time for an update, process 108 | # the info. 109 | update_check = time.time() 110 | if packets and update_check - last_update >= update_interval: 111 | last_update = update_check # Refresh our update timestamp 112 | speed = " | ?? B/s" 113 | if len(packets) > 1: 114 | # Let's calculate the amount downloaded over how long 115 | try: 116 | first,last = packets[0][0],packets[-1][0] 117 | chunks = sum([float(x[1]) for x in packets]) 118 | t = last-first 119 | assert t >= 0 120 | bytes_speed = 1. / t * chunks 121 | speed = " | {}/s".format(get_size(bytes_speed,round_to=1)) 122 | # Get our remaining time 123 | if total_size > 0: 124 | seconds_left = (total_size-bytes_so_far) / bytes_speed 125 | days = seconds_left // 86400 126 | hours = (seconds_left - (days*86400)) // 3600 127 | mins = (seconds_left - (days*86400) - (hours*3600)) // 60 128 | secs = seconds_left - (days*86400) - (hours*3600) - (mins*60) 129 | if days > 99 or bytes_speed == 0: 130 | remaining = " | ?? left" 131 | else: 132 | remaining = " | {}{:02d}:{:02d}:{:02d} left".format( 133 | "{}:".format(int(days)) if days else "", 134 | int(hours), 135 | int(mins), 136 | int(round(secs)) 137 | ) 138 | except: 139 | pass 140 | # Clear the packets so we don't reuse the same ones 141 | packets = [] 142 | 143 | class Downloader: 144 | 145 | def __init__(self,**kwargs): 146 | self.ua = kwargs.get("useragent",{"User-Agent":"Mozilla"}) 147 | self.chunk = 1048576 # 1024 x 1024 i.e. 1MiB 148 | if os.name=="nt": os.system("color") # Initialize cmd for ANSI escapes 149 | # Provide reasonable default logic to workaround macOS CA file handling 150 | cafile = ssl.get_default_verify_paths().openssl_cafile 151 | try: 152 | # If default OpenSSL CA file does not exist, use that from certifi 153 | if not os.path.exists(cafile): 154 | import certifi 155 | cafile = certifi.where() 156 | self.ssl_context = ssl.create_default_context(cafile=cafile) 157 | except: 158 | # None of the above worked, disable certificate verification for now 159 | self.ssl_context = ssl._create_unverified_context() 160 | return 161 | 162 | def _decode(self, value, encoding="utf-8", errors="ignore"): 163 | # Helper method to only decode if bytes type 164 | if sys.version_info >= (3,0) and isinstance(value, bytes): 165 | return value.decode(encoding,errors) 166 | return value 167 | 168 | def _update_main_name(self): 169 | # Windows running python 2 seems to have issues with multiprocessing 170 | # if the case of the main script's name is incorrect: 171 | # e.g. Downloader.py vs downloader.py 172 | # 173 | # To work around this, we try to scrape for the correct case if 174 | # possible. 175 | try: 176 | path = os.path.abspath(sys.modules["__main__"].__file__) 177 | except AttributeError as e: 178 | # This likely means we're running from the interpreter 179 | # directly 180 | return None 181 | if not os.path.isfile(path): 182 | return None 183 | # Get the file name and folder path 184 | name = os.path.basename(path).lower() 185 | fldr = os.path.dirname(path) 186 | # Walk the files in the folder until we find our 187 | # name - then steal its case and update that path 188 | for f in os.listdir(fldr): 189 | if f.lower() == name: 190 | # Got it 191 | new_path = os.path.join(fldr,f) 192 | sys.modules["__main__"].__file__ = new_path 193 | return new_path 194 | # If we got here, it wasn't found 195 | return None 196 | 197 | def _get_headers(self, headers = None): 198 | # Fall back on the default ua if none provided 199 | target = headers if isinstance(headers,dict) else self.ua 200 | new_headers = {} 201 | # Shallow copy to prevent changes to the headers 202 | # overriding the original 203 | for k in target: 204 | new_headers[k] = target[k] 205 | return new_headers 206 | 207 | def open_url(self, url, headers = None): 208 | headers = self._get_headers(headers) 209 | # Wrap up the try/except block so we don't have to do this for each function 210 | try: 211 | response = urlopen(Request(url, headers=headers), context=self.ssl_context) 212 | except Exception as e: 213 | # No fixing this - bail 214 | return None 215 | return response 216 | 217 | def get_size(self, *args, **kwargs): 218 | return get_size(*args,**kwargs) 219 | 220 | def get_string(self, url, progress = True, headers = None, expand_gzip = True): 221 | response = self.get_bytes(url,progress,headers,expand_gzip) 222 | if response is None: return None 223 | return self._decode(response) 224 | 225 | def get_bytes(self, url, progress = True, headers = None, expand_gzip = True): 226 | response = self.open_url(url, headers) 227 | if response is None: return None 228 | try: total_size = int(response.headers['Content-Length']) 229 | except: total_size = -1 230 | chunk_so_far = b"" 231 | packets = queue = process = None 232 | if progress: 233 | # Make sure our vars are initialized 234 | packets = [] if progress else None 235 | queue = multiprocessing.Queue() 236 | # Create the multiprocess and start it 237 | process = multiprocessing.Process( 238 | target=_process_hook, 239 | args=(queue,total_size) 240 | ) 241 | process.daemon = True 242 | # Filthy hack for earlier python versions on Windows 243 | if os.name == "nt" and hasattr(multiprocessing,"forking"): 244 | self._update_main_name() 245 | process.start() 246 | try: 247 | while True: 248 | chunk = response.read(self.chunk) 249 | if progress: 250 | # Add our items to the queue 251 | queue.put((time.time(),len(chunk))) 252 | if not chunk: break 253 | chunk_so_far += chunk 254 | finally: 255 | # Close the response whenever we're done 256 | response.close() 257 | if expand_gzip and response.headers.get("Content-Encoding","unknown").lower() == "gzip": 258 | fileobj = BytesIO(chunk_so_far) 259 | gfile = gzip.GzipFile(fileobj=fileobj) 260 | return gfile.read() 261 | if progress: 262 | # Finalize the queue and wait 263 | queue.put("DONE") 264 | process.join() 265 | return chunk_so_far 266 | 267 | def stream_to_file(self, url, file_path, progress = True, headers = None, ensure_size_if_present = True, allow_resume = False): 268 | response = self.open_url(url, headers) 269 | if response is None: return None 270 | bytes_so_far = 0 271 | try: total_size = int(response.headers['Content-Length']) 272 | except: total_size = -1 273 | packets = queue = process = None 274 | mode = "wb" 275 | if allow_resume and os.path.isfile(file_path) and total_size != -1: 276 | # File exists, we're resuming and have a target size. Check the 277 | # local file size. 278 | current_size = os.stat(file_path).st_size 279 | if current_size == total_size: 280 | # File is already complete - return the path 281 | return file_path 282 | elif current_size < total_size: 283 | response.close() 284 | # File is not complete - seek to our current size 285 | bytes_so_far = current_size 286 | mode = "ab" # Append 287 | # We also need to try creating a new request 288 | # in order to pass our range header 289 | new_headers = self._get_headers(headers) 290 | # Get the start byte, 0-indexed 291 | byte_string = "bytes={}-".format(current_size) 292 | new_headers["Range"] = byte_string 293 | response = self.open_url(url, new_headers) 294 | if response is None: return None 295 | if progress: 296 | # Make sure our vars are initialized 297 | packets = [] if progress else None 298 | queue = multiprocessing.Queue() 299 | # Create the multiprocess and start it 300 | process = multiprocessing.Process( 301 | target=_process_hook, 302 | args=(queue,total_size,bytes_so_far) 303 | ) 304 | process.daemon = True 305 | # Filthy hack for earlier python versions on Windows 306 | if os.name == "nt" and hasattr(multiprocessing,"forking"): 307 | self._update_main_name() 308 | process.start() 309 | with open(file_path,mode) as f: 310 | try: 311 | while True: 312 | chunk = response.read(self.chunk) 313 | bytes_so_far += len(chunk) 314 | if progress: 315 | # Add our items to the queue 316 | queue.put((time.time(),len(chunk))) 317 | if not chunk: break 318 | f.write(chunk) 319 | finally: 320 | # Close the response whenever we're done 321 | response.close() 322 | if progress: 323 | # Finalize the queue and wait 324 | queue.put("DONE") 325 | process.join() 326 | if ensure_size_if_present and total_size != -1: 327 | # We're verifying size - make sure we got what we asked for 328 | if bytes_so_far != total_size: 329 | return None # We didn't - imply it failed 330 | return file_path if os.path.exists(file_path) else None 331 | -------------------------------------------------------------------------------- /Scripts/dsdt.py: -------------------------------------------------------------------------------- 1 | import os, errno, tempfile, shutil, plistlib, sys, binascii, zipfile, getpass, re 2 | from . import run, downloader, utils 3 | 4 | try: 5 | FileNotFoundError 6 | except NameError: 7 | FileNotFoundError = IOError 8 | 9 | class DSDT: 10 | def __init__(self, **kwargs): 11 | self.dl = downloader.Downloader() 12 | self.r = run.Run() 13 | self.u = utils.Utils("SSDT Time") 14 | self.iasl_url_macOS = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-stable" 15 | self.iasl_url_macOS_legacy = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-legacy" 16 | self.iasl_url_linux = "https://raw.githubusercontent.com/corpnewt/linux_iasl/main/iasl.zip" 17 | self.iasl_url_linux_legacy = "https://raw.githubusercontent.com/corpnewt/iasl-legacy/main/iasl-legacy-linux.zip" 18 | self.acpi_github_windows = "https://github.com/acpica/acpica/releases/latest" 19 | self.acpi_binary_tools = "https://www.intel.com/content/www/us/en/developer/topic-technology/open/acpica/download.html" 20 | self.iasl_url_windows_legacy = "https://raw.githubusercontent.com/corpnewt/iasl-legacy/main/iasl-legacy-windows.zip" 21 | self.h = {} # {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} 22 | self.iasl = self.check_iasl() 23 | self.iasl_legacy = self.check_iasl(legacy=True) 24 | if not self.iasl: 25 | url = (self.acpi_github_windows,self.acpi_binary_tools) if os.name=="nt" else \ 26 | self.iasl_url_macOS if sys.platform=="darwin" else \ 27 | self.iasl_url_linux if sys.platform.startswith("linux") else None 28 | exception = "Could not locate or download iasl!" 29 | if url: 30 | exception += "\n\nPlease manually download {} from:\n - {}\n\nAnd place in:\n - {}\n".format( 31 | "and extract iasl.exe and acpidump.exe" if os.name=="nt" else "iasl", 32 | "\n - ".join(url) if isinstance(url,(list,tuple)) else url, 33 | os.path.dirname(os.path.realpath(__file__)) 34 | ) 35 | raise Exception(exception) 36 | self.allowed_signatures = (b"APIC",b"DMAR",b"DSDT",b"SSDT") 37 | self.mixed_listing = (b"DSDT",b"SSDT") 38 | self.acpi_tables = {} 39 | # Setup regex matches 40 | self.hex_match = re.compile(r"^\s*[0-9A-F]{4,}:(\s[0-9A-F]{2})+(\s+\/\/.*)?$") 41 | self.type_match = re.compile(r".*(?PProcessor|Scope|Device|Method|Name) \((?P[^,\)]+).*") 42 | 43 | def _table_signature(self, table_path, table_name = None, data = None): 44 | path = os.path.join(table_path,table_name) if table_name else table_path 45 | if not os.path.isfile(path): 46 | return None 47 | if data: 48 | # Got data - make sure there's enough for a signature 49 | if len(data) >= 4: 50 | return data[:4] 51 | else: 52 | return None 53 | # Try to load it and read the first 4 bytes to verify the 54 | # signature 55 | with open(path,"rb") as f: 56 | try: 57 | return f.read(4) 58 | except: 59 | pass 60 | return None 61 | 62 | def non_ascii_count(self, data): 63 | # Helper to emulate the ACPI_IS_ASCII macro from ACPICA's code 64 | # It just appears to check if the passed byte is < 0x80 65 | # We'll check all available data though - and return the number 66 | # of non-ascii bytes 67 | non_ascii = 0 68 | for b in data: 69 | if not isinstance(b,int): 70 | try: b = ord(b) 71 | except: b = -1 72 | if not b < 0x80: 73 | non_ascii += 1 74 | return non_ascii 75 | 76 | def table_is_valid(self, table_path, table_name = None, ensure_binary = True, check_signature = True): 77 | # Ensure we have a valid file 78 | path = os.path.join(table_path,table_name) if table_name else table_path 79 | if not os.path.isfile(path): 80 | return False 81 | # Set up a data placeholder 82 | data = None 83 | if ensure_binary is not None: 84 | # Make sure the table is the right type - load it 85 | # and read the data 86 | with open(path,"rb") as f: 87 | data = f.read() 88 | # Make sure we actually got some data 89 | if not data: 90 | return False 91 | # Gather the non-ASCII char count 92 | non_ascii_count = self.non_ascii_count(data) 93 | if ensure_binary and not non_ascii_count: 94 | # We want a binary, but it's all ascii 95 | return False 96 | elif not ensure_binary and non_ascii_count: 97 | # We want ascii, and got a binary 98 | return False 99 | if check_signature: 100 | if not self._table_signature(path,data=data) in self.allowed_signatures: 101 | # Check with the function - we didn't load the table 102 | # already 103 | return False 104 | # If we got here - the table passed our checks 105 | return True 106 | 107 | def get_ascii_print(self, data): 108 | # Helper to sanitize unprintable characters by replacing them with 109 | # ? where needed 110 | unprintables = False 111 | ascii_string = "" 112 | for b in data: 113 | if not isinstance(b,int): 114 | try: b = ord(b) 115 | except: b = -1 116 | if ord(" ") <= b < ord("~"): 117 | ascii_string += chr(b) 118 | else: 119 | ascii_string += "?" 120 | unprintables = True 121 | return (unprintables,ascii_string) 122 | 123 | def load(self, table_path): 124 | # Attempt to load the passed file - or if a directory 125 | # was passed, load all .aml and .dat files within 126 | cwd = os.getcwd() 127 | temp = None 128 | target_files = {} 129 | failed = [] 130 | try: 131 | if os.path.isdir(table_path): 132 | # Got a directory - gather all valid 133 | # files in the directory 134 | valid_files = [ 135 | x for x in os.listdir(table_path) if self.table_is_valid(table_path,x) 136 | ] 137 | elif os.path.isfile(table_path): 138 | # Just loading the one table - don't check 139 | # the signature - but make sure it's binary 140 | if self.table_is_valid(table_path,check_signature=False): 141 | valid_files = [table_path] 142 | else: 143 | # Not valid - raise an error 144 | raise FileNotFoundError( 145 | errno.ENOENT, 146 | os.strerror(errno.ENOENT), 147 | "{} is not a valid .aml/.dat file.".format(table_path) 148 | ) 149 | else: 150 | # Not a valid path 151 | raise FileNotFoundError( 152 | errno.ENOENT, 153 | os.strerror(errno.ENOENT), 154 | table_path 155 | ) 156 | if not valid_files: 157 | # No valid files were found 158 | raise FileNotFoundError( 159 | errno.ENOENT, 160 | os.strerror(errno.ENOENT), 161 | "No valid .aml/.dat files found at {}".format(table_path) 162 | ) 163 | # Create a temp dir and copy all files there 164 | temp = tempfile.mkdtemp() 165 | for file in valid_files: 166 | shutil.copy( 167 | os.path.join(table_path,file), 168 | temp 169 | ) 170 | # Build a list of all target files in the temp folder - and save 171 | # the disassembled_name for each to verify after 172 | list_dir = os.listdir(temp) 173 | for x in list_dir: 174 | if len(list_dir) > 1 and not self.table_is_valid(temp,x): 175 | continue # Skip invalid files when multiple are passed 176 | name_ext = [y for y in os.path.basename(x).split(".") if y] 177 | if name_ext and name_ext[-1].lower() in ("asl","dsl"): 178 | continue # Skip any already disassembled files 179 | target_files[x] = { 180 | "assembled_name": os.path.basename(x), 181 | "disassembled_name": ".".join(x.split(".")[:-1]) + ".dsl", 182 | } 183 | if not target_files: 184 | # Somehow we ended up with none? 185 | raise FileNotFoundError( 186 | errno.ENOENT, 187 | os.strerror(errno.ENOENT), 188 | "No valid .aml/.dat files found at {}".format(table_path) 189 | ) 190 | os.chdir(temp) 191 | # Generate and run a command 192 | dsdt_or_ssdt = [x for x in list(target_files) if self._table_signature(temp,x) in self.mixed_listing] 193 | other_tables = [x for x in list(target_files) if not x in dsdt_or_ssdt] 194 | out_d = ("","",0) 195 | out_t = ("","",0) 196 | 197 | def exists(folder_path,file_name): 198 | # Helper to make sure the file exists and has a non-Zero size 199 | check_path = os.path.join(folder_path,file_name) 200 | if os.path.isfile(check_path) and os.stat(check_path).st_size > 0: 201 | return True 202 | return False 203 | 204 | # Check our DSDT and SSDTs first 205 | if dsdt_or_ssdt: 206 | args = [self.iasl,"-da","-dl","-l"]+list(dsdt_or_ssdt) 207 | out_d = self.r.run({"args":args}) 208 | if out_d[2] != 0: 209 | # Attempt to run without `-da` if the above failed 210 | args = [self.iasl,"-dl","-l"]+list(dsdt_or_ssdt) 211 | out_d = self.r.run({"args":args}) 212 | # Get a list of disassembled names that failed 213 | fail_temp = [] 214 | for x in dsdt_or_ssdt: 215 | if not exists(temp,target_files[x]["disassembled_name"]): 216 | fail_temp.append(x) 217 | # Let's try to disassemble any that failed individually 218 | for x in fail_temp: 219 | args = [self.iasl,"-dl","-l",x] 220 | self.r.run({"args":args}) 221 | if not exists(temp,target_files[x]["disassembled_name"]): 222 | failed.append(x) 223 | # Check for other tables (DMAR, APIC, etc) 224 | if other_tables: 225 | args = [self.iasl]+list(other_tables) 226 | out_t = self.r.run({"args":args}) 227 | # Get a list of disassembled names that failed 228 | for x in other_tables: 229 | if not exists(temp,target_files[x]["disassembled_name"]): 230 | failed.append(x) 231 | if len(failed) == len(target_files): 232 | raise Exception("Failed to disassemble - {}".format(", ".join(failed))) 233 | # Actually process the tables now 234 | to_remove = [] 235 | for file in target_files: 236 | # We need to load the .aml and .dsl into memory 237 | # and get the paths and scopes 238 | if not exists(temp,target_files[file]["disassembled_name"]): 239 | to_remove.append(file) 240 | continue 241 | with open(os.path.join(temp,target_files[file]["disassembled_name"]),"r") as f: 242 | target_files[file]["table"] = f.read() 243 | # Remove the compiler info at the start 244 | if target_files[file]["table"].startswith("/*"): 245 | target_files[file]["table"] = "*/".join(target_files[file]["table"].split("*/")[1:]).strip() 246 | # Check for "Table Header:" or "Raw Table Data: Length" and strip everything 247 | # after the last occurrence 248 | for h in ("\nTable Header:","\nRaw Table Data: Length"): 249 | if h in target_files[file]["table"]: 250 | target_files[file]["table"] = h.join(target_files[file]["table"].split(h)[:-1]).rstrip() 251 | break # Bail on the first match 252 | target_files[file]["lines"] = target_files[file]["table"].split("\n") 253 | target_files[file]["scopes"] = self.get_scopes(table=target_files[file]) 254 | target_files[file]["paths"] = self.get_paths(table=target_files[file]) 255 | with open(os.path.join(temp,file),"rb") as f: 256 | table_bytes = f.read() 257 | target_files[file]["raw"] = table_bytes 258 | # Let's read the table header and get the info we need 259 | # 260 | # [0:4] = Table Signature 261 | # [4:8] = Length (little endian) 262 | # [8] = Compliance Revision 263 | # [9] = Checksum 264 | # [10:16] = OEM ID (6 chars, padded to the right with \x00) 265 | # [16:24] = Table ID (8 chars, padded to the right with \x00) 266 | # [24:28] = OEM Revision (little endian) 267 | # 268 | target_files[file]["signature"] = table_bytes[0:4] 269 | target_files[file]["revision"] = table_bytes[8] 270 | target_files[file]["oem"] = table_bytes[10:16] 271 | target_files[file]["id"] = table_bytes[16:24] 272 | target_files[file]["oem_revision"] = int(binascii.hexlify(table_bytes[24:28][::-1]),16) 273 | target_files[file]["length"] = len(table_bytes) 274 | # Get the printable versions of the sig, oem, and id as needed 275 | for key in ("signature","oem","id"): 276 | unprintable,ascii_string = self.get_ascii_print(target_files[file][key]) 277 | if unprintable: 278 | target_files[file][key+"_ascii"] = ascii_string 279 | # Cast as int on py2, and try to decode bytes to strings on py3 280 | if 2/3==0: 281 | target_files[file]["revision"] = int(binascii.hexlify(target_files[file]["revision"]),16) 282 | # The disassembler omits the last line of hex data in a mixed listing 283 | # file... convenient. However - we should be able to reconstruct this 284 | # manually. 285 | last_hex = next((l for l in target_files[file]["lines"][::-1] if self.is_hex(l)),None) 286 | if last_hex: 287 | # Get the address left of the colon 288 | addr = int(last_hex.split(":")[0].strip(),16) 289 | # Get the hex bytes right of the colon 290 | hexs = last_hex.split(":")[1].split("//")[0].strip() 291 | # Increment the address by the number of hex bytes 292 | next_addr = addr+len(hexs.split()) 293 | # Now we need to get the bytes at the end 294 | hexb = self.get_hex_bytes(hexs.replace(" ","")) 295 | # Get the last occurrence after the split 296 | remaining = target_files[file]["raw"].split(hexb)[-1] 297 | else: 298 | # If we didn't get a last hex val - then we likely don't have any 299 | # This can happen if the file passed is small enough, or has all 300 | # the data in a single block. 301 | next_addr = 0 302 | remaining = target_files[file]["raw"] 303 | # Iterate in chunks of 16 304 | for chunk in [remaining[i:i+16] for i in range(0,len(remaining),16)]: 305 | # Build a new byte string 306 | hex_string = binascii.hexlify(chunk) 307 | # Decode the bytes if we're on python 3 308 | if 2/3!=0: hex_string = hex_string.decode() 309 | # Ensure the bytes are all upper case 310 | hex_string = hex_string.upper() 311 | l = " {}: {}".format( 312 | hex(next_addr)[2:].upper().rjust(4,"0"), 313 | " ".join([hex_string[i:i+2] for i in range(0,len(hex_string),2)]) 314 | ) 315 | # Increment our address 316 | next_addr += len(chunk) 317 | # Append our line 318 | target_files[file]["lines"].append(l) 319 | target_files[file]["table"] += "\n"+l 320 | # Remove any that didn't disassemble 321 | for file in to_remove: 322 | target_files.pop(file,None) 323 | except Exception as e: 324 | print(e) 325 | return ({},failed) 326 | finally: 327 | os.chdir(cwd) 328 | if temp: shutil.rmtree(temp,ignore_errors=True) 329 | # Add/update any tables we loaded 330 | for table in target_files: 331 | self.acpi_tables[table] = target_files[table] 332 | # Only return the newly loaded results 333 | return (target_files, failed,) 334 | 335 | def get_latest_iasl(self): 336 | # First try getting from github - if that fails, fall back to intel.com 337 | try: 338 | source = self.dl.get_string(self.acpi_github_windows, progress=False, headers=self.h) 339 | assets_url = None 340 | # Check for attachments first 341 | for line in source.split("\n"): 342 | if 'iasl compiler and windows acpi tools" in line.lower(): 373 | # Check if we have a direct download link - i.e. ends with .zip - or if we're 374 | # redirected to a different download page - i.e. ends with .html 375 | dl_link = line.split('iASL Compiler and Windows ACPI Tools 387 | # Only a suffix - prepend to it 388 | dl_page_url = "https://www.intel.com" + line.split(' {} failed: {}".format(f,new_name,e)) 503 | print("Dump successful!") 504 | if disassemble: 505 | return self.load(res) 506 | return res 507 | else: 508 | print("Failed to locate acpidump.exe") 509 | return 510 | elif sys.platform.startswith("linux"): 511 | table_dir = "/sys/firmware/acpi/tables" 512 | if not os.path.isdir(table_dir): 513 | print("Could not locate {}!".format(table_dir)) 514 | return 515 | print("Copying tables to {}...".format(res)) 516 | copied_files = [] 517 | for table in os.listdir(table_dir): 518 | if not os.path.isfile(os.path.join(table_dir,table)): 519 | continue # We only want files 520 | target_path = os.path.join(res,table.upper()+".aml") 521 | comms = ( 522 | # Copy the file 523 | ["sudo","cp",os.path.join(table_dir,table),target_path], 524 | # Ensure it's owned by the user account 525 | ["sudo","chown",getpass.getuser(),target_path], 526 | # Enable read and write permissions 527 | ["sudo","chmod","a+rw",target_path] 528 | ) 529 | # Iterate our commands and bail if any error 530 | for comm in comms: 531 | out = self.r.run({"args":comm}) 532 | if check_command_output(out): 533 | return 534 | print("Dump successful!") 535 | if disassemble: 536 | return self.load(res) 537 | return res 538 | 539 | def check_output(self, output): 540 | t_folder = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), output) 541 | if not os.path.isdir(t_folder): 542 | os.makedirs(t_folder) 543 | return t_folder 544 | 545 | def get_hex_from_int(self, total, pad_to = 4): 546 | hex_str = hex(total)[2:].upper().rjust(pad_to,"0") 547 | return "".join([hex_str[i:i + 2] for i in range(0, len(hex_str), 2)][::-1]) 548 | 549 | def get_hex(self, line): 550 | # strip the header and commented end 551 | return line.split(":")[1].split("//")[0].replace(" ","") 552 | 553 | def get_line(self, line): 554 | # Strip the header and commented end - no space replacing though 555 | line = line.split("//")[0] 556 | if ":" in line: 557 | return line.split(":")[1] 558 | return line 559 | 560 | def get_hex_bytes(self, line): 561 | return binascii.unhexlify(line) 562 | 563 | def get_str_bytes(self, value): 564 | if 2/3!=0 and isinstance(value,str): 565 | value = value.encode() 566 | return value 567 | 568 | def get_table_with_id(self, table_id): 569 | table_id = self.get_str_bytes(table_id) 570 | return next((v for k,v in self.acpi_tables.items() if table_id == v.get("id")),None) 571 | 572 | def get_table_with_signature(self, table_sig): 573 | table_sig = self.get_str_bytes(table_sig) 574 | return next((v for k,v in self.acpi_tables.items() if table_sig == v.get("signature")),None) 575 | 576 | def get_table(self, table_id_or_sig): 577 | table_id_or_sig = self.get_str_bytes(table_id_or_sig) 578 | return next((v for k,v in self.acpi_tables.items() if table_id_or_sig in (v.get("signature"),v.get("id"))),None) 579 | 580 | def get_dsdt(self): 581 | return self.get_table_with_signature("DSDT") 582 | 583 | def get_dsdt_or_only(self): 584 | dsdt = self.get_dsdt() 585 | if dsdt: return dsdt 586 | # Make sure we have only one table 587 | if len(self.acpi_tables) != 1: 588 | return None 589 | return list(self.acpi_tables.values())[0] 590 | 591 | def find_previous_hex(self, index=0, table=None): 592 | if not table: table = self.get_dsdt_or_only() 593 | if not table: return ("",-1,-1) 594 | # Returns the index of the previous set of hex digits before the passed index 595 | start_index = -1 596 | end_index = -1 597 | old_hex = True 598 | for i,line in enumerate(table.get("lines","")[index::-1]): 599 | if old_hex: 600 | if not self.is_hex(line): 601 | # Broke out of the old hex 602 | old_hex = False 603 | continue 604 | # Not old_hex territory - check if we got new hex 605 | if self.is_hex(line): # Checks for a :, but not in comments 606 | end_index = index-i 607 | hex_text,start_index = self.get_hex_ending_at(end_index,table=table) 608 | return (hex_text, start_index, end_index) 609 | return ("",start_index,end_index) 610 | 611 | def find_next_hex(self, index=0, table=None): 612 | if not table: table = self.get_dsdt_or_only() 613 | if not table: return ("",-1,-1) 614 | # Returns the index of the next set of hex digits after the passed index 615 | start_index = -1 616 | end_index = -1 617 | old_hex = True 618 | for i,line in enumerate(table.get("lines","")[index:]): 619 | if old_hex: 620 | if not self.is_hex(line): 621 | # Broke out of the old hex 622 | old_hex = False 623 | continue 624 | # Not old_hex territory - check if we got new hex 625 | if self.is_hex(line): # Checks for a :, but not in comments 626 | start_index = i+index 627 | hex_text,end_index = self.get_hex_starting_at(start_index,table=table) 628 | return (hex_text, start_index, end_index) 629 | return ("",start_index,end_index) 630 | 631 | def is_hex(self, line): 632 | return self.hex_match.match(line) is not None 633 | 634 | def get_hex_starting_at(self, start_index, table=None): 635 | if not table: table = self.get_dsdt_or_only() 636 | if not table: return ("",-1) 637 | # Returns a tuple of the hex, and the ending index 638 | hex_text = "" 639 | index = -1 640 | for i,x in enumerate(table.get("lines","")[start_index:]): 641 | if not self.is_hex(x): 642 | break 643 | hex_text += self.get_hex(x) 644 | index = i+start_index 645 | return (hex_text, index) 646 | 647 | def get_hex_ending_at(self, start_index, table=None): 648 | if not table: table = self.get_dsdt_or_only() 649 | if not table: return ("",-1) 650 | # Returns a tuple of the hex, and the ending index 651 | hex_text = "" 652 | index = -1 653 | for i,x in enumerate(table.get("lines","")[start_index::-1]): 654 | if not self.is_hex(x): 655 | break 656 | hex_text = self.get_hex(x)+hex_text 657 | index = start_index-i 658 | return (hex_text, index) 659 | 660 | def get_shortest_unique_pad(self, current_hex, index, instance=0, table=None): 661 | if not table: table = self.get_dsdt_or_only() 662 | if not table: return None 663 | try: left_pad = self.get_unique_pad(current_hex, index, False, instance, table=table) 664 | except: left_pad = None 665 | try: right_pad = self.get_unique_pad(current_hex, index, True, instance, table=table) 666 | except: right_pad = None 667 | try: mid_pad = self.get_unique_pad(current_hex, index, None, instance, table=table) 668 | except: mid_pad = None 669 | if left_pad == right_pad == mid_pad is None: raise Exception("No unique pad found!") 670 | # We got at least one unique pad 671 | min_pad = None 672 | for x in (left_pad,right_pad,mid_pad): 673 | if x is None: continue # Skip 674 | if min_pad is None or len(x[0]+x[1]) < len(min_pad[0]+min_pad[1]): 675 | min_pad = x 676 | return min_pad 677 | 678 | def get_unique_pad(self, current_hex, index, direction=None, instance=0, table=None): 679 | if not table: table = self.get_dsdt_or_only() 680 | if not table: raise Exception("No valid table passed!") 681 | # Returns any pad needed to make the passed patch unique 682 | # direction can be True = forward, False = backward, None = both 683 | start_index = index 684 | line,last_index = self.get_hex_starting_at(index,table=table) 685 | if last_index == -1: 686 | raise Exception("Could not find hex starting at index {}!".format(index)) 687 | first_line = line 688 | # Assume at least 1 byte of our current_hex exists at index, so we need to at 689 | # least load in len(current_hex)-2 worth of data if we haven't found it. 690 | while True: 691 | if current_hex in line or len(line) >= len(first_line)+len(current_hex): 692 | break # Assume we've hit our cap 693 | new_line,_index,last_index = self.find_next_hex(last_index, table=table) 694 | if last_index == -1: 695 | raise Exception("Hit end of file before passed hex was located!") 696 | # Append the new info 697 | line += new_line 698 | if not current_hex in line: 699 | raise Exception("{} not found in table at index {}-{}!".format(current_hex,start_index,last_index)) 700 | padl = padr = "" 701 | parts = line.split(current_hex) 702 | if instance >= len(parts)-1: 703 | raise Exception("Instance out of range!") 704 | linel = current_hex.join(parts[0:instance+1]) 705 | liner = current_hex.join(parts[instance+1:]) 706 | last_check = True # Default to forward 707 | while True: 708 | # Check if our hex string is unique 709 | check_bytes = self.get_hex_bytes(padl+current_hex+padr) 710 | if table["raw"].count(check_bytes) == 1: # Got it! 711 | break 712 | if direction == True or (direction is None and len(padr)<=len(padl)): 713 | # Let's check a forward byte 714 | if not len(liner): 715 | # Need to grab more 716 | liner, _index, last_index = self.find_next_hex(last_index, table=table) 717 | if last_index == -1: raise Exception("Hit end of file before unique hex was found!") 718 | padr = padr+liner[0:2] 719 | liner = liner[2:] 720 | continue 721 | if direction == False or (direction is None and len(padl)<=len(padr)): 722 | # Let's check a backward byte 723 | if not len(linel): 724 | # Need to grab more 725 | linel, start_index, _index = self.find_previous_hex(start_index, table=table) 726 | if _index == -1: raise Exception("Hit end of file before unique hex was found!") 727 | padl = linel[-2:]+padl 728 | linel = linel[:-2] 729 | continue 730 | break 731 | return (padl,padr) 732 | 733 | def get_devices(self,search=None,types=("Device (","Scope ("),strip_comments=False,table=None): 734 | if not table: table = self.get_dsdt_or_only() 735 | if not table: return [] 736 | # Returns a list of tuples organized as (Device/Scope,d_s_index,matched_index) 737 | if search is None: 738 | return [] 739 | last_device = None 740 | device_index = 0 741 | devices = [] 742 | for index,line in enumerate(table.get("lines","")): 743 | if self.is_hex(line): 744 | continue 745 | line = self.get_line(line) if strip_comments else line 746 | if any ((x for x in types if x in line)): 747 | # Got a last_device match 748 | last_device = line 749 | device_index = index 750 | if search in line: 751 | # Got a search hit - add it 752 | devices.append((last_device,device_index,index)) 753 | return devices 754 | 755 | def get_scope(self,starting_index=0,add_hex=False,strip_comments=False,table=None): 756 | if not table: table = self.get_dsdt_or_only() 757 | if not table: return [] 758 | # Walks the scope starting at starting_index, and returns when 759 | # we've exited 760 | brackets = None 761 | scope = [] 762 | for line in table.get("lines","")[starting_index:]: 763 | if self.is_hex(line): 764 | if add_hex: 765 | scope.append(line) 766 | continue 767 | line = self.get_line(line) if strip_comments else line 768 | scope.append(line) 769 | if brackets is None: 770 | if line.count("{"): 771 | brackets = line.count("{") 772 | continue 773 | brackets = brackets + line.count("{") - line.count("}") 774 | if brackets <= 0: 775 | # We've exited the scope 776 | return scope 777 | return scope 778 | 779 | def get_scopes(self, table=None): 780 | if not table: table = self.get_dsdt_or_only() 781 | if not table: return [] 782 | scopes = [] 783 | for index,line in enumerate(table.get("lines","")): 784 | if self.is_hex(line): continue 785 | if any(x in line for x in ("Processor (","Scope (","Device (","Method (","Name (")): 786 | scopes.append((line,index)) 787 | return scopes 788 | 789 | def get_paths(self, table=None): 790 | if not table: table = self.get_dsdt_or_only() 791 | if not table: return [] 792 | # Set up lists for complete paths, as well 793 | # as our current path reference 794 | path_list = [] 795 | _path = [] 796 | brackets = 0 797 | for i,line in enumerate(table.get("lines",[])): 798 | if self.is_hex(line): 799 | # Skip hex 800 | continue 801 | line = self.get_line(line) 802 | brackets += line.count("{")-line.count("}") 803 | while len(_path): 804 | # Remove any path entries that are nested 805 | # equal to or further than our current set 806 | if _path[-1][-1] >= brackets: 807 | del _path[-1] 808 | else: 809 | break 810 | type_match = self.type_match.match(line) 811 | if type_match: 812 | # Add our path entry and save the full path 813 | # to the path list as needed 814 | _path.append((type_match.group("name"),brackets)) 815 | if type_match.group("type") == "Scope": 816 | continue 817 | # Ensure that we only consider non-Scope paths that aren't 818 | # already fully qualified with a \ prefix 819 | path = [] 820 | for p in _path[::-1]: 821 | path.append(p[0]) 822 | p_check = p[0].split(".")[0].rstrip("_") 823 | if p_check.startswith("\\") or p_check in ("_SB","_PR"): 824 | # Fully qualified - bail here 825 | break 826 | path = ".".join(path[::-1]).split(".") 827 | # Properly qualify the path 828 | if len(path) and path[0] == "\\": path.pop(0) 829 | if any("^" in x for x in path): # Accommodate caret notation 830 | new_path = [] 831 | for x in path: 832 | if x.count("^"): 833 | # Remove the last Y paths to account for going up a level 834 | del new_path[-1*x.count("^"):] 835 | new_path.append(x.replace("^","")) # Add the original, removing any ^ chars 836 | path = new_path 837 | if not path: 838 | continue 839 | # Ensure we strip trailing underscores for consistency 840 | padded_path = [("\\" if j==0 else"")+x.lstrip("\\").rstrip("_") for j,x in enumerate(path)] 841 | path_str = ".".join(padded_path) 842 | path_list.append((path_str,i,type_match.group("type"))) 843 | return sorted(path_list) 844 | 845 | def get_path_of_type(self, obj_type="Device", obj="HPET", table=None): 846 | if not table: table = self.get_dsdt_or_only() 847 | if not table: return [] 848 | paths = [] 849 | # Remove trailing underscores and normalize case for all path 850 | # elements passed 851 | obj = ".".join([x.rstrip("_").upper() for x in obj.split(".")]) 852 | obj_type = obj_type.lower() if obj_type else obj_type 853 | for path in table.get("paths",[]): 854 | path_check = ".".join([x.rstrip("_").upper() for x in path[0].split(".")]) 855 | if (obj_type and obj_type != path[2].lower()) or not path_check.endswith(obj): 856 | # Type or object mismatch - skip 857 | continue 858 | paths.append(path) 859 | return sorted(paths) 860 | 861 | def get_device_paths(self, obj="HPET",table=None): 862 | return self.get_path_of_type(obj_type="Device",obj=obj,table=table) 863 | 864 | def get_method_paths(self, obj="_STA",table=None): 865 | return self.get_path_of_type(obj_type="Method",obj=obj,table=table) 866 | 867 | def get_name_paths(self, obj="CPU0",table=None): 868 | return self.get_path_of_type(obj_type="Name",obj=obj,table=table) 869 | 870 | def get_processor_paths(self, obj_type="Processor",table=None): 871 | return self.get_path_of_type(obj_type=obj_type,obj="",table=table) 872 | 873 | def get_device_paths_with_id(self,_id="PNP0A03",id_types=("_HID","_CID"),table=None): 874 | if not table: table = self.get_dsdt_or_only() 875 | if not table: return [] 876 | if not isinstance(id_types,(list,tuple)): return [] 877 | # Strip non-strings from the list 878 | id_types = [x.upper() for x in id_types if isinstance(x,str)] 879 | if not id_types: return [] 880 | _id = _id.upper() # Ensure case 881 | devs = [] 882 | for p in table.get("paths",[]): 883 | try: 884 | for type_check in id_types: 885 | if p[0].endswith(type_check) and _id in table.get("lines")[p[1]]: 886 | # Save the path, strip the suffix and trailing periods 887 | devs.append(p[0][:-len(type_check)].rstrip(".")) 888 | # Leave this loop to avoid adding the same device 889 | # multiple times 890 | break 891 | except Exception as e: 892 | print(e) 893 | continue 894 | devices = [] 895 | # Walk the paths again - and save any devices 896 | # that match our prior list 897 | for p in table.get("paths",[]): 898 | if p[0] in devs and p[-1] == "Device": 899 | devices.append(p) 900 | return devices 901 | 902 | def get_device_paths_with_cid(self,cid="PNP0A03",table=None): 903 | return self.get_device_paths_with_id(_id=cid,id_types=("_CID",),table=table) 904 | 905 | def get_device_paths_with_hid(self,hid="ACPI000E",table=None): 906 | return self.get_device_paths_with_id(_id=hid,id_types=("_HID",),table=table) 907 | -------------------------------------------------------------------------------- /Scripts/plist.py: -------------------------------------------------------------------------------- 1 | ### ### 2 | # Imports # 3 | ### ### 4 | 5 | import datetime, os, plistlib, struct, sys, itertools, binascii 6 | from io import BytesIO 7 | 8 | if sys.version_info < (3,0): 9 | # Force use of StringIO instead of cStringIO as the latter 10 | # has issues with Unicode strings 11 | from StringIO import StringIO 12 | else: 13 | from io import StringIO 14 | 15 | try: 16 | basestring # Python 2 17 | unicode 18 | except NameError: 19 | basestring = str # Python 3 20 | unicode = str 21 | 22 | try: 23 | FMT_XML = plistlib.FMT_XML 24 | FMT_BINARY = plistlib.FMT_BINARY 25 | except AttributeError: 26 | FMT_XML = "FMT_XML" 27 | FMT_BINARY = "FMT_BINARY" 28 | 29 | ### ### 30 | # Helper Methods # 31 | ### ### 32 | 33 | def wrap_data(value): 34 | if not _check_py3(): return plistlib.Data(value) 35 | return value 36 | 37 | def extract_data(value): 38 | if not _check_py3() and isinstance(value,plistlib.Data): return value.data 39 | return value 40 | 41 | def _check_py3(): 42 | return sys.version_info >= (3, 0) 43 | 44 | def _is_binary(fp): 45 | if isinstance(fp, basestring): 46 | return fp.startswith(b"bplist00") 47 | header = fp.read(32) 48 | fp.seek(0) 49 | return header[:8] == b'bplist00' 50 | 51 | ### ### 52 | # Deprecated Functions - Remapped # 53 | ### ### 54 | 55 | def readPlist(pathOrFile): 56 | if not isinstance(pathOrFile, basestring): 57 | return load(pathOrFile) 58 | with open(pathOrFile, "rb") as f: 59 | return load(f) 60 | 61 | def writePlist(value, pathOrFile): 62 | if not isinstance(pathOrFile, basestring): 63 | return dump(value, pathOrFile, fmt=FMT_XML, sort_keys=True, skipkeys=False) 64 | with open(pathOrFile, "wb") as f: 65 | return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False) 66 | 67 | ### ### 68 | # Remapped Functions # 69 | ### ### 70 | 71 | def load(fp, fmt=None, use_builtin_types=None, dict_type=dict): 72 | if _is_binary(fp): 73 | use_builtin_types = False if use_builtin_types is None else use_builtin_types 74 | try: 75 | p = _BinaryPlistParser(use_builtin_types=use_builtin_types, dict_type=dict_type) 76 | except: 77 | # Python 3.9 removed use_builtin_types 78 | p = _BinaryPlistParser(dict_type=dict_type) 79 | return p.parse(fp) 80 | elif _check_py3(): 81 | use_builtin_types = True if use_builtin_types is None else use_builtin_types 82 | # We need to monkey patch this to allow for hex integers - code taken/modified from 83 | # https://github.com/python/cpython/blob/3.8/Lib/plistlib.py 84 | if fmt is None: 85 | header = fp.read(32) 86 | fp.seek(0) 87 | for info in plistlib._FORMATS.values(): 88 | if info['detect'](header): 89 | P = info['parser'] 90 | break 91 | else: 92 | raise plistlib.InvalidFileException() 93 | else: 94 | P = plistlib._FORMATS[fmt]['parser'] 95 | try: 96 | p = P(use_builtin_types=use_builtin_types, dict_type=dict_type) 97 | except: 98 | # Python 3.9 removed use_builtin_types 99 | p = P(dict_type=dict_type) 100 | if isinstance(p,plistlib._PlistParser): 101 | # Monkey patch! 102 | def end_integer(): 103 | d = p.get_data() 104 | value = int(d,16) if d.lower().startswith("0x") else int(d) 105 | if -1 << 63 <= value < 1 << 64: 106 | p.add_object(value) 107 | else: 108 | raise OverflowError("Integer overflow at line {}".format(p.parser.CurrentLineNumber)) 109 | def end_data(): 110 | try: 111 | p.add_object(plistlib._decode_base64(p.get_data())) 112 | except Exception as e: 113 | raise Exception("Data error at line {}: {}".format(p.parser.CurrentLineNumber,e)) 114 | p.end_integer = end_integer 115 | p.end_data = end_data 116 | return p.parse(fp) 117 | else: 118 | # Is not binary - assume a string - and try to load 119 | # We avoid using readPlistFromString() as that uses 120 | # cStringIO and fails when Unicode strings are detected 121 | # Don't subclass - keep the parser local 122 | from xml.parsers.expat import ParserCreate 123 | # Create a new PlistParser object - then we need to set up 124 | # the values and parse. 125 | p = plistlib.PlistParser() 126 | parser = ParserCreate() 127 | parser.StartElementHandler = p.handleBeginElement 128 | parser.EndElementHandler = p.handleEndElement 129 | parser.CharacterDataHandler = p.handleData 130 | # We also need to monkey patch this to allow for other dict_types, hex int support 131 | # proper line output for data errors, and for unicode string decoding 132 | def begin_dict(attrs): 133 | d = dict_type() 134 | p.addObject(d) 135 | p.stack.append(d) 136 | def end_integer(): 137 | d = p.getData() 138 | value = int(d,16) if d.lower().startswith("0x") else int(d) 139 | if -1 << 63 <= value < 1 << 64: 140 | p.addObject(value) 141 | else: 142 | raise OverflowError("Integer overflow at line {}".format(parser.CurrentLineNumber)) 143 | def end_data(): 144 | try: 145 | p.addObject(plistlib.Data.fromBase64(p.getData())) 146 | except Exception as e: 147 | raise Exception("Data error at line {}: {}".format(parser.CurrentLineNumber,e)) 148 | def end_string(): 149 | d = p.getData() 150 | if isinstance(d,unicode): 151 | d = d.encode("utf-8") 152 | p.addObject(d) 153 | p.begin_dict = begin_dict 154 | p.end_integer = end_integer 155 | p.end_data = end_data 156 | p.end_string = end_string 157 | if isinstance(fp, unicode): 158 | # Encode unicode -> string; use utf-8 for safety 159 | fp = fp.encode("utf-8") 160 | if isinstance(fp, basestring): 161 | # It's a string - let's wrap it up 162 | fp = StringIO(fp) 163 | # Parse it 164 | parser.ParseFile(fp) 165 | return p.root 166 | 167 | def loads(value, fmt=None, use_builtin_types=None, dict_type=dict): 168 | if _check_py3() and isinstance(value, basestring): 169 | # If it's a string - encode it 170 | value = value.encode() 171 | try: 172 | return load(BytesIO(value),fmt=fmt,use_builtin_types=use_builtin_types,dict_type=dict_type) 173 | except: 174 | # Python 3.9 removed use_builtin_types 175 | return load(BytesIO(value),fmt=fmt,dict_type=dict_type) 176 | 177 | def dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False): 178 | if fmt == FMT_BINARY: 179 | # Assume binary at this point 180 | writer = _BinaryPlistWriter(fp, sort_keys=sort_keys, skipkeys=skipkeys) 181 | writer.write(value) 182 | elif fmt == FMT_XML: 183 | if _check_py3(): 184 | plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys) 185 | else: 186 | # We need to monkey patch a bunch here too in order to avoid auto-sorting 187 | # of keys 188 | writer = plistlib.PlistWriter(fp) 189 | def writeDict(d): 190 | if d: 191 | writer.beginElement("dict") 192 | items = sorted(d.items()) if sort_keys else d.items() 193 | for key, value in items: 194 | if not isinstance(key, basestring): 195 | if skipkeys: 196 | continue 197 | raise TypeError("keys must be strings") 198 | writer.simpleElement("key", key) 199 | writer.writeValue(value) 200 | writer.endElement("dict") 201 | else: 202 | writer.simpleElement("dict") 203 | writer.writeDict = writeDict 204 | writer.writeln("") 205 | writer.writeValue(value) 206 | writer.writeln("") 207 | else: 208 | # Not a proper format 209 | raise ValueError("Unsupported format: {}".format(fmt)) 210 | 211 | def dumps(value, fmt=FMT_XML, skipkeys=False, sort_keys=True): 212 | # We avoid using writePlistToString() as that uses 213 | # cStringIO and fails when Unicode strings are detected 214 | f = BytesIO() if _check_py3() else StringIO() 215 | dump(value, f, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys) 216 | value = f.getvalue() 217 | if _check_py3(): 218 | value = value.decode("utf-8") 219 | return value 220 | 221 | ### ### 222 | # Binary Plist Stuff For Py2 # 223 | ### ### 224 | 225 | # From the python 3 plistlib.py source: https://github.com/python/cpython/blob/3.11/Lib/plistlib.py 226 | # Tweaked to function on both Python 2 and 3 227 | 228 | class UID: 229 | def __init__(self, data): 230 | if not isinstance(data, int): 231 | raise TypeError("data must be an int") 232 | # It seems Apple only uses 32-bit unsigned ints for UIDs. Although the comment in 233 | # CoreFoundation's CFBinaryPList.c detailing the binary plist format theoretically 234 | # allows for 64-bit UIDs, most functions in the same file use 32-bit unsigned ints, 235 | # with the sole function hinting at 64-bits appearing to be a leftover from copying 236 | # and pasting integer handling code internally, and this code has not changed since 237 | # it was added. (In addition, code in CFPropertyList.c to handle CF$UID also uses a 238 | # 32-bit unsigned int.) 239 | # 240 | # if data >= 1 << 64: 241 | # raise ValueError("UIDs cannot be >= 2**64") 242 | if data >= 1 << 32: 243 | raise ValueError("UIDs cannot be >= 2**32 (4294967296)") 244 | if data < 0: 245 | raise ValueError("UIDs must be positive") 246 | self.data = data 247 | 248 | def __index__(self): 249 | return self.data 250 | 251 | def __repr__(self): 252 | return "%s(%s)" % (self.__class__.__name__, repr(self.data)) 253 | 254 | def __reduce__(self): 255 | return self.__class__, (self.data,) 256 | 257 | def __eq__(self, other): 258 | if not isinstance(other, UID): 259 | return NotImplemented 260 | return self.data == other.data 261 | 262 | def __hash__(self): 263 | return hash(self.data) 264 | 265 | class InvalidFileException (ValueError): 266 | def __init__(self, message="Invalid file"): 267 | ValueError.__init__(self, message) 268 | 269 | _BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'} 270 | 271 | _undefined = object() 272 | 273 | class _BinaryPlistParser: 274 | """ 275 | Read or write a binary plist file, following the description of the binary 276 | format. Raise InvalidFileException in case of error, otherwise return the 277 | root object. 278 | see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c 279 | """ 280 | def __init__(self, use_builtin_types, dict_type): 281 | self._use_builtin_types = use_builtin_types 282 | self._dict_type = dict_type 283 | 284 | def parse(self, fp): 285 | try: 286 | # The basic file format: 287 | # HEADER 288 | # object... 289 | # refid->offset... 290 | # TRAILER 291 | self._fp = fp 292 | self._fp.seek(-32, os.SEEK_END) 293 | trailer = self._fp.read(32) 294 | if len(trailer) != 32: 295 | raise InvalidFileException() 296 | ( 297 | offset_size, self._ref_size, num_objects, top_object, 298 | offset_table_offset 299 | ) = struct.unpack('>6xBBQQQ', trailer) 300 | self._fp.seek(offset_table_offset) 301 | self._object_offsets = self._read_ints(num_objects, offset_size) 302 | self._objects = [_undefined] * num_objects 303 | return self._read_object(top_object) 304 | 305 | except (OSError, IndexError, struct.error, OverflowError, 306 | UnicodeDecodeError): 307 | raise InvalidFileException() 308 | 309 | def _get_size(self, tokenL): 310 | """ return the size of the next object.""" 311 | if tokenL == 0xF: 312 | m = self._fp.read(1)[0] 313 | if not _check_py3(): 314 | m = ord(m) 315 | m = m & 0x3 316 | s = 1 << m 317 | f = '>' + _BINARY_FORMAT[s] 318 | return struct.unpack(f, self._fp.read(s))[0] 319 | 320 | return tokenL 321 | 322 | def _read_ints(self, n, size): 323 | data = self._fp.read(size * n) 324 | if size in _BINARY_FORMAT: 325 | return struct.unpack('>' + _BINARY_FORMAT[size] * n, data) 326 | else: 327 | if not size or len(data) != size * n: 328 | raise InvalidFileException() 329 | return tuple(int(binascii.hexlify(data[i: i + size]),16) 330 | for i in range(0, size * n, size)) 331 | '''return tuple(int.from_bytes(data[i: i + size], 'big') 332 | for i in range(0, size * n, size))''' 333 | 334 | def _read_refs(self, n): 335 | return self._read_ints(n, self._ref_size) 336 | 337 | def _read_object(self, ref): 338 | """ 339 | read the object by reference. 340 | May recursively read sub-objects (content of an array/dict/set) 341 | """ 342 | result = self._objects[ref] 343 | if result is not _undefined: 344 | return result 345 | 346 | offset = self._object_offsets[ref] 347 | self._fp.seek(offset) 348 | token = self._fp.read(1)[0] 349 | if not _check_py3(): 350 | token = ord(token) 351 | tokenH, tokenL = token & 0xF0, token & 0x0F 352 | 353 | if token == 0x00: # \x00 or 0x00 354 | result = None 355 | 356 | elif token == 0x08: # \x08 or 0x08 357 | result = False 358 | 359 | elif token == 0x09: # \x09 or 0x09 360 | result = True 361 | 362 | # The referenced source code also mentions URL (0x0c, 0x0d) and 363 | # UUID (0x0e), but neither can be generated using the Cocoa libraries. 364 | 365 | elif token == 0x0f: # \x0f or 0x0f 366 | result = b'' 367 | 368 | elif tokenH == 0x10: # int 369 | result = int(binascii.hexlify(self._fp.read(1 << tokenL)),16) 370 | if tokenL >= 3: # Signed - adjust 371 | result = result-((result & 0x8000000000000000) << 1) 372 | 373 | elif token == 0x22: # real 374 | result = struct.unpack('>f', self._fp.read(4))[0] 375 | 376 | elif token == 0x23: # real 377 | result = struct.unpack('>d', self._fp.read(8))[0] 378 | 379 | elif token == 0x33: # date 380 | f = struct.unpack('>d', self._fp.read(8))[0] 381 | # timestamp 0 of binary plists corresponds to 1/1/2001 382 | # (year of Mac OS X 10.0), instead of 1/1/1970. 383 | result = (datetime.datetime(2001, 1, 1) + 384 | datetime.timedelta(seconds=f)) 385 | 386 | elif tokenH == 0x40: # data 387 | s = self._get_size(tokenL) 388 | if self._use_builtin_types or not hasattr(plistlib, "Data"): 389 | result = self._fp.read(s) 390 | else: 391 | result = plistlib.Data(self._fp.read(s)) 392 | 393 | elif tokenH == 0x50: # ascii string 394 | s = self._get_size(tokenL) 395 | result = self._fp.read(s).decode('ascii') 396 | result = result 397 | 398 | elif tokenH == 0x60: # unicode string 399 | s = self._get_size(tokenL) 400 | result = self._fp.read(s * 2).decode('utf-16be') 401 | 402 | elif tokenH == 0x80: # UID 403 | # used by Key-Archiver plist files 404 | result = UID(int(binascii.hexlify(self._fp.read(1 + tokenL)),16)) 405 | 406 | elif tokenH == 0xA0: # array 407 | s = self._get_size(tokenL) 408 | obj_refs = self._read_refs(s) 409 | result = [] 410 | self._objects[ref] = result 411 | result.extend(self._read_object(x) for x in obj_refs) 412 | 413 | # tokenH == 0xB0 is documented as 'ordset', but is not actually 414 | # implemented in the Apple reference code. 415 | 416 | # tokenH == 0xC0 is documented as 'set', but sets cannot be used in 417 | # plists. 418 | 419 | elif tokenH == 0xD0: # dict 420 | s = self._get_size(tokenL) 421 | key_refs = self._read_refs(s) 422 | obj_refs = self._read_refs(s) 423 | result = self._dict_type() 424 | self._objects[ref] = result 425 | for k, o in zip(key_refs, obj_refs): 426 | key = self._read_object(k) 427 | if hasattr(plistlib, "Data") and isinstance(key, plistlib.Data): 428 | key = key.data 429 | result[key] = self._read_object(o) 430 | 431 | else: 432 | raise InvalidFileException() 433 | 434 | self._objects[ref] = result 435 | return result 436 | 437 | def _count_to_size(count): 438 | if count < 1 << 8: 439 | return 1 440 | 441 | elif count < 1 << 16: 442 | return 2 443 | 444 | elif count < 1 << 32: 445 | return 4 446 | 447 | else: 448 | return 8 449 | 450 | _scalars = (str, int, float, datetime.datetime, bytes) 451 | 452 | class _BinaryPlistWriter (object): 453 | def __init__(self, fp, sort_keys, skipkeys): 454 | self._fp = fp 455 | self._sort_keys = sort_keys 456 | self._skipkeys = skipkeys 457 | 458 | def write(self, value): 459 | 460 | # Flattened object list: 461 | self._objlist = [] 462 | 463 | # Mappings from object->objectid 464 | # First dict has (type(object), object) as the key, 465 | # second dict is used when object is not hashable and 466 | # has id(object) as the key. 467 | self._objtable = {} 468 | self._objidtable = {} 469 | 470 | # Create list of all objects in the plist 471 | self._flatten(value) 472 | 473 | # Size of object references in serialized containers 474 | # depends on the number of objects in the plist. 475 | num_objects = len(self._objlist) 476 | self._object_offsets = [0]*num_objects 477 | self._ref_size = _count_to_size(num_objects) 478 | 479 | self._ref_format = _BINARY_FORMAT[self._ref_size] 480 | 481 | # Write file header 482 | self._fp.write(b'bplist00') 483 | 484 | # Write object list 485 | for obj in self._objlist: 486 | self._write_object(obj) 487 | 488 | # Write refnum->object offset table 489 | top_object = self._getrefnum(value) 490 | offset_table_offset = self._fp.tell() 491 | offset_size = _count_to_size(offset_table_offset) 492 | offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects 493 | self._fp.write(struct.pack(offset_format, *self._object_offsets)) 494 | 495 | # Write trailer 496 | sort_version = 0 497 | trailer = ( 498 | sort_version, offset_size, self._ref_size, num_objects, 499 | top_object, offset_table_offset 500 | ) 501 | self._fp.write(struct.pack('>5xBBBQQQ', *trailer)) 502 | 503 | def _flatten(self, value): 504 | # First check if the object is in the object table, not used for 505 | # containers to ensure that two subcontainers with the same contents 506 | # will be serialized as distinct values. 507 | if isinstance(value, _scalars): 508 | if (type(value), value) in self._objtable: 509 | return 510 | 511 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 512 | if (type(value.data), value.data) in self._objtable: 513 | return 514 | 515 | elif id(value) in self._objidtable: 516 | return 517 | 518 | # Add to objectreference map 519 | refnum = len(self._objlist) 520 | self._objlist.append(value) 521 | if isinstance(value, _scalars): 522 | self._objtable[(type(value), value)] = refnum 523 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 524 | self._objtable[(type(value.data), value.data)] = refnum 525 | else: 526 | self._objidtable[id(value)] = refnum 527 | 528 | # And finally recurse into containers 529 | if isinstance(value, dict): 530 | keys = [] 531 | values = [] 532 | items = value.items() 533 | if self._sort_keys: 534 | items = sorted(items) 535 | 536 | for k, v in items: 537 | if not isinstance(k, basestring): 538 | if self._skipkeys: 539 | continue 540 | raise TypeError("keys must be strings") 541 | keys.append(k) 542 | values.append(v) 543 | 544 | for o in itertools.chain(keys, values): 545 | self._flatten(o) 546 | 547 | elif isinstance(value, (list, tuple)): 548 | for o in value: 549 | self._flatten(o) 550 | 551 | def _getrefnum(self, value): 552 | if isinstance(value, _scalars): 553 | return self._objtable[(type(value), value)] 554 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 555 | return self._objtable[(type(value.data), value.data)] 556 | else: 557 | return self._objidtable[id(value)] 558 | 559 | def _write_size(self, token, size): 560 | if size < 15: 561 | self._fp.write(struct.pack('>B', token | size)) 562 | 563 | elif size < 1 << 8: 564 | self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size)) 565 | 566 | elif size < 1 << 16: 567 | self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size)) 568 | 569 | elif size < 1 << 32: 570 | self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size)) 571 | 572 | else: 573 | self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size)) 574 | 575 | def _write_object(self, value): 576 | ref = self._getrefnum(value) 577 | self._object_offsets[ref] = self._fp.tell() 578 | if value is None: 579 | self._fp.write(b'\x00') 580 | 581 | elif value is False: 582 | self._fp.write(b'\x08') 583 | 584 | elif value is True: 585 | self._fp.write(b'\x09') 586 | 587 | elif isinstance(value, int): 588 | if value < 0: 589 | try: 590 | self._fp.write(struct.pack('>Bq', 0x13, value)) 591 | except struct.error: 592 | raise OverflowError(value) # from None 593 | elif value < 1 << 8: 594 | self._fp.write(struct.pack('>BB', 0x10, value)) 595 | elif value < 1 << 16: 596 | self._fp.write(struct.pack('>BH', 0x11, value)) 597 | elif value < 1 << 32: 598 | self._fp.write(struct.pack('>BL', 0x12, value)) 599 | elif value < 1 << 63: 600 | self._fp.write(struct.pack('>BQ', 0x13, value)) 601 | elif value < 1 << 64: 602 | self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True)) 603 | else: 604 | raise OverflowError(value) 605 | 606 | elif isinstance(value, float): 607 | self._fp.write(struct.pack('>Bd', 0x23, value)) 608 | 609 | elif isinstance(value, datetime.datetime): 610 | f = (value - datetime.datetime(2001, 1, 1)).total_seconds() 611 | self._fp.write(struct.pack('>Bd', 0x33, f)) 612 | 613 | elif (_check_py3() and isinstance(value, (bytes, bytearray))) or (hasattr(plistlib, "Data") and isinstance(value, plistlib.Data)): 614 | if not isinstance(value, (bytes, bytearray)): 615 | value = value.data # Unpack it 616 | self._write_size(0x40, len(value)) 617 | self._fp.write(value) 618 | 619 | elif isinstance(value, basestring): 620 | try: 621 | t = value.encode('ascii') 622 | self._write_size(0x50, len(value)) 623 | except UnicodeEncodeError: 624 | t = value.encode('utf-16be') 625 | self._write_size(0x60, len(t) // 2) 626 | self._fp.write(t) 627 | 628 | elif isinstance(value, UID) or (hasattr(plistlib,"UID") and isinstance(value, plistlib.UID)): 629 | if value.data < 0: 630 | raise ValueError("UIDs must be positive") 631 | elif value.data < 1 << 8: 632 | self._fp.write(struct.pack('>BB', 0x80, value)) 633 | elif value.data < 1 << 16: 634 | self._fp.write(struct.pack('>BH', 0x81, value)) 635 | elif value.data < 1 << 32: 636 | self._fp.write(struct.pack('>BL', 0x83, value)) 637 | # elif value.data < 1 << 64: 638 | # self._fp.write(struct.pack('>BQ', 0x87, value)) 639 | else: 640 | raise OverflowError(value) 641 | 642 | elif isinstance(value, (list, tuple)): 643 | refs = [self._getrefnum(o) for o in value] 644 | s = len(refs) 645 | self._write_size(0xA0, s) 646 | self._fp.write(struct.pack('>' + self._ref_format * s, *refs)) 647 | 648 | elif isinstance(value, dict): 649 | keyRefs, valRefs = [], [] 650 | 651 | if self._sort_keys: 652 | rootItems = sorted(value.items()) 653 | else: 654 | rootItems = value.items() 655 | 656 | for k, v in rootItems: 657 | if not isinstance(k, basestring): 658 | if self._skipkeys: 659 | continue 660 | raise TypeError("keys must be strings") 661 | keyRefs.append(self._getrefnum(k)) 662 | valRefs.append(self._getrefnum(v)) 663 | 664 | s = len(keyRefs) 665 | self._write_size(0xD0, s) 666 | self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs)) 667 | self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs)) 668 | 669 | else: 670 | raise TypeError(value) 671 | -------------------------------------------------------------------------------- /Scripts/reveal.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | from . import run 3 | 4 | class Reveal: 5 | 6 | def __init__(self): 7 | self.r = run.Run() 8 | return 9 | 10 | def get_parent(self, path): 11 | return os.path.normpath(os.path.join(path, os.pardir)) 12 | 13 | def reveal(self, path, new_window = False): 14 | # Reveals the passed path in Finder - only works on macOS 15 | if not sys.platform == "darwin": 16 | return ("", "macOS Only", 1) 17 | if not path: 18 | # No path sent - nothing to reveal 19 | return ("", "No path specified", 1) 20 | # Build our script - then convert it to a single line task 21 | if not os.path.exists(path): 22 | # Not real - bail 23 | return ("", "{} - doesn't exist".format(path), 1) 24 | # Get the absolute path 25 | path = os.path.abspath(path) 26 | command = ["osascript"] 27 | if new_window: 28 | command.extend([ 29 | "-e", "set p to \"{}\"".format(path.replace("\"", "\\\"")), 30 | "-e", "tell application \"Finder\"", 31 | "-e", "reveal POSIX file p as text", 32 | "-e", "activate", 33 | "-e", "end tell" 34 | ]) 35 | else: 36 | if path == self.get_parent(path): 37 | command.extend([ 38 | "-e", "set p to \"{}\"".format(path.replace("\"", "\\\"")), 39 | "-e", "tell application \"Finder\"", 40 | "-e", "reopen", 41 | "-e", "activate", 42 | "-e", "set target of window 1 to (POSIX file p as text)", 43 | "-e", "end tell" 44 | ]) 45 | else: 46 | command.extend([ 47 | "-e", "set o to \"{}\"".format(self.get_parent(path).replace("\"", "\\\"")), 48 | "-e", "set p to \"{}\"".format(path.replace("\"", "\\\"")), 49 | "-e", "tell application \"Finder\"", 50 | "-e", "reopen", 51 | "-e", "activate", 52 | "-e", "set target of window 1 to (POSIX file o as text)", 53 | "-e", "select (POSIX file p as text)", 54 | "-e", "end tell" 55 | ]) 56 | return self.r.run({"args" : command}) 57 | 58 | def notify(self, title = None, subtitle = None, sound = None): 59 | # Sends a notification 60 | if not title: 61 | return ("", "Malformed dict", 1) 62 | # Build our notification 63 | n_text = "display notification with title \"{}\"".format(title.replace("\"", "\\\"")) 64 | if subtitle: 65 | n_text += " subtitle \"{}\"".format(subtitle.replace("\"", "\\\"")) 66 | if sound: 67 | n_text += " sound name \"{}\"".format(sound.replace("\"", "\\\"")) 68 | command = ["osascript", "-e", n_text] 69 | return self.r.run({"args" : command}) 70 | -------------------------------------------------------------------------------- /Scripts/run.py: -------------------------------------------------------------------------------- 1 | import sys, subprocess, time, threading, shlex 2 | try: 3 | from Queue import Queue, Empty 4 | except: 5 | from queue import Queue, Empty 6 | 7 | ON_POSIX = 'posix' in sys.builtin_module_names 8 | 9 | class Run: 10 | 11 | def __init__(self): 12 | return 13 | 14 | def _read_output(self, pipe, q): 15 | try: 16 | for line in iter(lambda: pipe.read(1), b''): 17 | q.put(line) 18 | except ValueError: 19 | pass 20 | pipe.close() 21 | 22 | def _create_thread(self, output): 23 | # Creates a new queue and thread object to watch based on the output pipe sent 24 | q = Queue() 25 | t = threading.Thread(target=self._read_output, args=(output, q)) 26 | t.daemon = True 27 | return (q,t) 28 | 29 | def _stream_output(self, comm, shell = False): 30 | output = error = "" 31 | p = None 32 | try: 33 | if shell and type(comm) is list: 34 | comm = " ".join(shlex.quote(x) for x in comm) 35 | if not shell and type(comm) is str: 36 | comm = shlex.split(comm) 37 | p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, universal_newlines=True, close_fds=ON_POSIX) 38 | # Setup the stdout thread/queue 39 | q,t = self._create_thread(p.stdout) 40 | qe,te = self._create_thread(p.stderr) 41 | # Start both threads 42 | t.start() 43 | te.start() 44 | 45 | while True: 46 | c = z = "" 47 | try: c = q.get_nowait() 48 | except Empty: pass 49 | else: 50 | sys.stdout.write(c) 51 | output += c 52 | sys.stdout.flush() 53 | try: z = qe.get_nowait() 54 | except Empty: pass 55 | else: 56 | sys.stderr.write(z) 57 | error += z 58 | sys.stderr.flush() 59 | if not c==z=="": continue # Keep going until empty 60 | # No output - see if still running 61 | p.poll() 62 | if p.returncode != None: 63 | # Subprocess ended 64 | break 65 | # No output, but subprocess still running - stall for 20ms 66 | time.sleep(0.02) 67 | 68 | o, e = p.communicate() 69 | return (output+o, error+e, p.returncode) 70 | except: 71 | if p: 72 | try: o, e = p.communicate() 73 | except: o = e = "" 74 | return (output+o, error+e, p.returncode) 75 | return ("", "Command not found!", 1) 76 | 77 | def _decode(self, value, encoding="utf-8", errors="ignore"): 78 | # Helper method to only decode if bytes type 79 | if sys.version_info >= (3,0) and isinstance(value, bytes): 80 | return value.decode(encoding,errors) 81 | return value 82 | 83 | def _run_command(self, comm, shell = False): 84 | c = None 85 | try: 86 | if shell and type(comm) is list: 87 | comm = " ".join(shlex.quote(x) for x in comm) 88 | if not shell and type(comm) is str: 89 | comm = shlex.split(comm) 90 | p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 91 | c = p.communicate() 92 | except: 93 | if c == None: 94 | return ("", "Command not found!", 1) 95 | return (self._decode(c[0]), self._decode(c[1]), p.returncode) 96 | 97 | def run(self, command_list, leave_on_fail = False): 98 | # Command list should be an array of dicts 99 | if type(command_list) is dict: 100 | # We only have one command 101 | command_list = [command_list] 102 | output_list = [] 103 | for comm in command_list: 104 | args = comm.get("args", []) 105 | shell = comm.get("shell", False) 106 | stream = comm.get("stream", False) 107 | sudo = comm.get("sudo", False) 108 | stdout = comm.get("stdout", False) 109 | stderr = comm.get("stderr", False) 110 | mess = comm.get("message", None) 111 | show = comm.get("show", False) 112 | 113 | if not mess == None: 114 | print(mess) 115 | 116 | if not len(args): 117 | # nothing to process 118 | continue 119 | if sudo: 120 | # Check if we have sudo 121 | out = self._run_command(["which", "sudo"]) 122 | if "sudo" in out[0]: 123 | # Can sudo 124 | if type(args) is list: 125 | args.insert(0, out[0].replace("\n", "")) # add to start of list 126 | elif type(args) is str: 127 | args = out[0].replace("\n", "") + " " + args # add to start of string 128 | 129 | if show: 130 | print(" ".join(args)) 131 | 132 | if stream: 133 | # Stream it! 134 | out = self._stream_output(args, shell) 135 | else: 136 | # Just run and gather output 137 | out = self._run_command(args, shell) 138 | if stdout and len(out[0]): 139 | print(out[0]) 140 | if stderr and len(out[1]): 141 | print(out[1]) 142 | # Append output 143 | output_list.append(out) 144 | # Check for errors 145 | if leave_on_fail and out[2] != 0: 146 | # Got an error - leave 147 | break 148 | if len(output_list) == 1: 149 | # We only ran one command - just return that output 150 | return output_list[0] 151 | return output_list 152 | -------------------------------------------------------------------------------- /Scripts/utils.py: -------------------------------------------------------------------------------- 1 | import sys, os, time, re, json, datetime, ctypes, subprocess 2 | 3 | if os.name == "nt": 4 | # Windows 5 | import msvcrt 6 | else: 7 | # Not Windows \o/ 8 | import select 9 | 10 | class Utils: 11 | 12 | def __init__(self, name = "Python Script"): 13 | self.name = name 14 | # Init our colors before we need to print anything 15 | cwd = os.getcwd() 16 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 17 | if os.path.exists("colors.json"): 18 | self.colors_dict = json.load(open("colors.json")) 19 | else: 20 | self.colors_dict = {} 21 | os.chdir(cwd) 22 | 23 | def check_admin(self): 24 | # Returns whether or not we're admin 25 | try: 26 | is_admin = os.getuid() == 0 27 | except AttributeError: 28 | is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 29 | return is_admin 30 | 31 | def elevate(self, file): 32 | # Runs the passed file as admin 33 | if self.check_admin(): 34 | return 35 | if os.name == "nt": 36 | ctypes.windll.shell32.ShellExecuteW(None, "runas", '"{}"'.format(sys.executable), '"{}"'.format(file), None, 1) 37 | else: 38 | try: 39 | p = subprocess.Popen(["which", "sudo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 40 | c = p.communicate()[0].decode("utf-8", "ignore").replace("\n", "") 41 | os.execv(c, [ sys.executable, 'python'] + sys.argv) 42 | except: 43 | exit(1) 44 | 45 | def compare_versions(self, vers1, vers2, **kwargs): 46 | # Helper method to compare ##.## strings 47 | # 48 | # vers1 < vers2 = True 49 | # vers1 = vers2 = None 50 | # vers1 > vers2 = False 51 | 52 | # Sanitize the pads 53 | pad = str(kwargs.get("pad", "")) 54 | sep = str(kwargs.get("separator", ".")) 55 | 56 | ignore_case = kwargs.get("ignore_case", True) 57 | 58 | # Cast as strings 59 | vers1 = str(vers1) 60 | vers2 = str(vers2) 61 | 62 | if ignore_case: 63 | vers1 = vers1.lower() 64 | vers2 = vers2.lower() 65 | 66 | # Split and pad lists 67 | v1_parts, v2_parts = self.pad_length(vers1.split(sep), vers2.split(sep)) 68 | 69 | # Iterate and compare 70 | for i in range(len(v1_parts)): 71 | # Remove non-numeric 72 | v1 = ''.join(c.lower() for c in v1_parts[i] if c.isalnum()) 73 | v2 = ''.join(c.lower() for c in v2_parts[i] if c.isalnum()) 74 | # Equalize the lengths 75 | v1, v2 = self.pad_length(v1, v2) 76 | # Compare 77 | if str(v1) < str(v2): 78 | return True 79 | elif str(v1) > str(v2): 80 | return False 81 | # Never differed - return None, must be equal 82 | return None 83 | 84 | def pad_length(self, var1, var2, pad = "0"): 85 | # Pads the vars on the left side to make them equal length 86 | pad = "0" if len(str(pad)) < 1 else str(pad)[0] 87 | if not type(var1) == type(var2): 88 | # Type mismatch! Just return what we got 89 | return (var1, var2) 90 | if len(var1) < len(var2): 91 | if type(var1) is list: 92 | var1.extend([str(pad) for x in range(len(var2) - len(var1))]) 93 | else: 94 | var1 = "{}{}".format((pad*(len(var2)-len(var1))), var1) 95 | elif len(var2) < len(var1): 96 | if type(var2) is list: 97 | var2.extend([str(pad) for x in range(len(var1) - len(var2))]) 98 | else: 99 | var2 = "{}{}".format((pad*(len(var1)-len(var2))), var2) 100 | return (var1, var2) 101 | 102 | def check_path(self, path): 103 | # Let's loop until we either get a working path, or no changes 104 | test_path = path 105 | last_path = None 106 | while True: 107 | # Bail if we've looped at least once and the path didn't change 108 | if last_path != None and last_path == test_path: return None 109 | last_path = test_path 110 | # Check if we stripped everything out 111 | if not len(test_path): return None 112 | # Check if we have a valid path 113 | if os.path.exists(test_path): 114 | return os.path.abspath(test_path) 115 | # Check for quotes 116 | if test_path[0] == test_path[-1] and test_path[0] in ('"',"'"): 117 | test_path = test_path[1:-1] 118 | continue 119 | # Check for a tilde and expand if needed 120 | if test_path[0] == "~": 121 | tilde_expanded = os.path.expanduser(test_path) 122 | if tilde_expanded != test_path: 123 | # Got a change 124 | test_path = tilde_expanded 125 | continue 126 | # Let's check for spaces - strip from the left first, then the right 127 | if test_path[0] in (" ","\t"): 128 | test_path = test_path[1:] 129 | continue 130 | if test_path[-1] in (" ","\t"): 131 | test_path = test_path[:-1] 132 | continue 133 | # Maybe we have escapes to handle? 134 | test_path = "\\".join([x.replace("\\", "") for x in test_path.split("\\\\")]) 135 | 136 | def grab(self, prompt, **kwargs): 137 | # Takes a prompt, a default, and a timeout and shows it with that timeout 138 | # returning the result 139 | timeout = kwargs.get("timeout",0) 140 | default = kwargs.get("default","") 141 | # If we don't have a timeout - then skip the timed sections 142 | if timeout <= 0: 143 | try: 144 | if sys.version_info >= (3, 0): 145 | return input(prompt) 146 | else: 147 | return str(raw_input(prompt)) 148 | except EOFError: 149 | return default 150 | # Write our prompt 151 | sys.stdout.write(prompt) 152 | sys.stdout.flush() 153 | if os.name == "nt": 154 | start_time = time.time() 155 | i = '' 156 | while True: 157 | if msvcrt.kbhit(): 158 | c = msvcrt.getche() 159 | if ord(c) == 13: # enter_key 160 | break 161 | elif ord(c) >= 32: # space_char 162 | i += c.decode() if sys.version_info >= (3,0) and isinstance(c,bytes) else c 163 | else: 164 | time.sleep(0.02) # Delay for 20ms to prevent CPU workload 165 | if len(i) == 0 and (time.time() - start_time) > timeout: 166 | break 167 | else: 168 | i, o, e = select.select( [sys.stdin], [], [], timeout ) 169 | if i: 170 | i = sys.stdin.readline().strip() 171 | print('') # needed to move to next line 172 | if len(i) > 0: 173 | return i 174 | else: 175 | return default 176 | 177 | def cls(self): 178 | if os.name == "nt": 179 | os.system("cls") 180 | elif os.environ.get("TERM"): 181 | os.system("clear") 182 | 183 | def cprint(self, message, **kwargs): 184 | strip_colors = kwargs.get("strip_colors", False) 185 | if os.name == "nt": 186 | strip_colors = True 187 | reset = u"\u001b[0m" 188 | # Requires sys import 189 | for c in self.colors: 190 | if strip_colors: 191 | message = message.replace(c["find"], "") 192 | else: 193 | message = message.replace(c["find"], c["replace"]) 194 | if strip_colors: 195 | return message 196 | sys.stdout.write(message) 197 | print(reset) 198 | 199 | # Needs work to resize the string if color chars exist 200 | '''# Header drawing method 201 | def head(self, text = None, width = 55): 202 | if text == None: 203 | text = self.name 204 | self.cls() 205 | print(" {}".format("#"*width)) 206 | len_text = self.cprint(text, strip_colors=True) 207 | mid_len = int(round(width/2-len(len_text)/2)-2) 208 | middle = " #{}{}{}#".format(" "*mid_len, len_text, " "*((width - mid_len - len(len_text))-2)) 209 | if len(middle) > width+1: 210 | # Get the difference 211 | di = len(middle) - width 212 | # Add the padding for the ...# 213 | di += 3 214 | # Trim the string 215 | middle = middle[:-di] 216 | newlen = len(middle) 217 | middle += "...#" 218 | find_list = [ c["find"] for c in self.colors ] 219 | 220 | # Translate colored string to len 221 | middle = middle.replace(len_text, text + self.rt_color) # always reset just in case 222 | self.cprint(middle) 223 | print("#"*width)''' 224 | 225 | # Header drawing method 226 | def head(self, text = None, width = 55): 227 | if text == None: 228 | text = self.name 229 | self.cls() 230 | print(" {}".format("#"*width)) 231 | mid_len = int(round(width/2-len(text)/2)-2) 232 | middle = " #{}{}{}#".format(" "*mid_len, text, " "*((width - mid_len - len(text))-2)) 233 | if len(middle) > width+1: 234 | # Get the difference 235 | di = len(middle) - width 236 | # Add the padding for the ...# 237 | di += 3 238 | # Trim the string 239 | middle = middle[:-di] + "...#" 240 | print(middle) 241 | print("#"*width) 242 | 243 | def resize(self, width, height): 244 | print('\033[8;{};{}t'.format(height, width)) 245 | 246 | def custom_quit(self): 247 | self.head() 248 | print("by CorpNewt\n") 249 | print("Thanks for testing it out, for bugs/comments/complaints") 250 | print("send me a message on Reddit, or check out my GitHub:\n") 251 | print("www.reddit.com/u/corpnewt") 252 | print("www.github.com/corpnewt\n") 253 | # Get the time and wish them a good morning, afternoon, evening, and night 254 | hr = datetime.datetime.now().time().hour 255 | if hr > 3 and hr < 12: 256 | print("Have a nice morning!\n\n") 257 | elif hr >= 12 and hr < 17: 258 | print("Have a nice afternoon!\n\n") 259 | elif hr >= 17 and hr < 21: 260 | print("Have a nice evening!\n\n") 261 | else: 262 | print("Have a nice night!\n\n") 263 | exit(0) 264 | --------------------------------------------------------------------------------