Skip to content

Commit d594e8d

Browse files
committed
Support more Powershell versions
Output is not always written to the terminal the same way. I think this is about the Powershell version. Make changes to line process for greater support. Other changes: - Add file logging
1 parent 660e57e commit d594e8d

File tree

6 files changed

+107
-33
lines changed

6 files changed

+107
-33
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,9 @@ The packages will be installed in reverse order with the last one specified inst
4343
main package (first argument) is installed last.
4444

4545
There is no tested limit on the number of dependencies.
46+
47+
## Logs
48+
49+
Logs are enabled by default.
50+
You can disable logging by changing ENABLE_LOG to 'False' in config.py.
51+
Logs are stored in 'C:\\Users\\USER\\AppData\\Local\\msix_global_installer\\msix_global_installer\\Logs'.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ requires-python = ">=3.10.6"
1010
dependencies = [
1111
"attrs>=24.3.0",
1212
"pillow>=11.1.0",
13+
"platformdirs>=4.3.6",
1314
"pyuac>=0.0.3",
1415
"pywin32>=308 ; sys_platform == 'win32'",
1516
"pywinpty>=2.0.14 ; sys_platform == 'win32'",

src/msix_global_installer/app.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,29 @@
22
import asyncio
33
import logging
44
import threading
5+
import platformdirs
6+
import pathlib
57

68

7-
logging.basicConfig(level=logging.NOTSET)
9+
if config.ENABLE_LOGS:
10+
log_dir_path = pathlib.Path(
11+
platformdirs.user_log_dir(appname="msix_global_installer", appauthor="msix_global_installer")
12+
)
13+
log_dir_path.mkdir(parents=True)
14+
log_path = log_dir_path / "installer.log"
15+
logging.basicConfig(
16+
level=logging.NOTSET,
17+
filename=log_path,
18+
filemode="a",
19+
format="%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s",
20+
datefmt="%H:%M:%S",
21+
)
22+
else:
23+
logging.basicConfig(
24+
level=logging.NOTSET,
25+
format="%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s",
26+
datefmt="%H:%M:%S",
27+
)
828
logger = logging.getLogger(__name__)
929

1030

src/msix_global_installer/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
from msix_global_installer import pyinstaller_helper
33

44
EXTRACTED_DATA_PATH: pathlib.Path = pyinstaller_helper.resource_path("extracted/data.pkl")
5-
ALLOW_DEPENDENCIES_TO_FAIL_DUE_TO_NEWER_VERSION_INSTALLED = True
5+
ALLOW_DEPENDENCIES_TO_FAIL_DUE_TO_NEWER_VERSION_INSTALLED = True
6+
ENABLE_LOGS = True

src/msix_global_installer/msix.py

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class ErrorResult:
3939

4040
@dataclass
4141
class ReturnCodeResult:
42-
return_code: int
42+
install_success: bool
4343

4444

4545
def get_msix_metadata(msix_path: str, output_icon_path: pathlib.Path | None = None) -> MsixMetadata:
@@ -201,13 +201,16 @@ def install_msix(
201201
):
202202
"""Install an MSIX package."""
203203
# TODO: If global install ensure we are running as admin
204-
global_install_command = "Add-AppxProvisionedPackage -PackagePath %s -Online -SkipLicense | Out-String" % path
205-
local_install_command = "Add-AppxPackage -Path %s | Out-String" % path
204+
global_install_command = (
205+
"Add-AppxProvisionedPackage -PackagePath %s -Online -SkipLicense -ErrorAction Continue | Out-String" % path
206+
)
207+
local_install_command = "Add-AppxPackage -Path %s -ErrorAction Continue | Out-String" % path
206208
command_string = local_install_command if not global_install else global_install_command
207-
save_returncode_string = "; $installRetcode = $LastExitCode"
208-
print_return_code = "; echo RETCODE=$installRetcode"
209+
# Use a q after the success int to confirm that we have read the right thing
210+
save_returncode_string = "; $success_tail='q' ; $success=[int][bool]::Parse($?)"
211+
print_return_code = "; echo INSTALL_SUCCESS===$success$success_tail"
209212
wait_string = "; Start-Sleep -Milliseconds 1500"
210-
exit_string = "; Exit"
213+
exit_string = "; echo Exiting with code $LASTEXITCODE; Exit"
211214

212215
# We must use a psudo terminal as otherwise
213216
# the written lines are not going to stdout, just appearing on the terminal for the progress
@@ -220,26 +223,35 @@ def install_msix(
220223
)
221224

222225
error: str | None = None
223-
retcode: int | None = None
226+
install_succeeded: bool | None = None
224227
while proc.isalive():
225228
line = proc.readline()
226229
logger.debug("%r\n\r", line)
227230
is_dependency = packages_to_install > 1 and package_number != packages_to_install
228231
result = process_line(line, is_dependency)
229232
# Return code will also come with a False for should continue so it doesn't
230233
# matter that we are overwriting this
231-
should_continue, retcode = process_result(
234+
should_continue, returned_install_result = process_result(
232235
result=result,
233236
package_title=title,
234237
current_error=error,
235238
packages_to_install=packages_to_install,
236239
package_number=package_number,
237240
)
241+
install_succeeded = returned_install_result
238242
if isinstance(result, ErrorResult):
239243
error = result.error if not error else error
240244
if not should_continue:
245+
logger.info("Received request to not continue!")
246+
# proc.write(exit_string + os.linesep)
241247
break
248+
logger.info("Continuing")
242249

250+
# TODO Work out if this actually returns the exit status of the terminal
251+
# It appears to always return 0
252+
logger.info("EXIT STATUS : %s", proc.exitstatus)
253+
if not install_succeeded:
254+
install_succeeded = True if proc.exitstatus == 0 else None
243255
logger.debug("Process is closed")
244256

245257
# Set progress to 100
@@ -251,16 +263,16 @@ def install_msix(
251263
)
252264
events.post_event_sync(event, event_queue=events.gui_event_queue)
253265

254-
return check_has_succeeded(return_code=retcode, error=error, package_title=title)
266+
return check_has_succeeded(install_succeeded=install_succeeded, error=error, package_title=title)
255267

256268

257-
def check_has_succeeded(return_code: int, error: str, package_title: str):
269+
def check_has_succeeded(install_succeeded: bool | None, error: str, package_title: str):
258270
"""
259271
Return success.
260272
261273
Post update to GUI on result.
262274
"""
263-
if return_code == 0 and not error:
275+
if install_succeeded is not None and install_succeeded and not error:
264276
logger.info("Should have installed successfully!")
265277
install_complete_text = f"Install of {package_title} complete"
266278
event = events.Event(
@@ -271,10 +283,11 @@ def check_has_succeeded(return_code: int, error: str, package_title: str):
271283
return True
272284
else:
273285
logger.error("Install failed")
274-
logger.error("Retcode is: %s", return_code)
286+
logger.error("App reported install succeeded: %s", install_succeeded)
275287
logger.error("Error is: %s", error)
276-
if error is None and return_code is None:
288+
if error is None and install_succeeded is None:
277289
# Terminal must have force quit - won't have an error message
290+
logger.warning("Stopping install - terminal must have force quit.")
278291
install_complete_text = f"Install of {package_title} failed"
279292
event = events.Event(
280293
name=events.EventType.INSTALL_PROGRESS_TEXT,
@@ -285,15 +298,17 @@ def check_has_succeeded(return_code: int, error: str, package_title: str):
285298

286299

287300
def process_result(
288-
result: ProgressResult | ErrorResult | ReturnCodeResult,
289-
current_error: str,
301+
result: ProgressResult | ErrorResult | ReturnCodeResult | None,
302+
current_error: str | None,
290303
package_title,
291304
packages_to_install,
292305
package_number,
293-
) -> tuple[bool, int | None]:
306+
) -> tuple[bool, bool | None]:
294307
"""Process a Result and return data to the GUI.
295308
296-
::returns:: Should Continue. Break on False return.
309+
::returns:: (should_continue, install_success)
310+
Should Continue: Break on False return.
311+
Install Success: Reported success of the script
297312
"""
298313
if isinstance(result, ProgressResult):
299314
event = events.Event(
@@ -318,16 +333,25 @@ def process_result(
318333
},
319334
)
320335
events.post_event_sync(event, event_queue=events.gui_event_queue)
336+
logger.warning("Stoppping install due to new error: %s", result.error)
337+
return (False, None)
321338
return (True, None)
322339
elif isinstance(result, ReturnCodeResult):
323-
retcode = result.return_code
324-
if retcode > 1 and current_error is None:
340+
install_succeeded = result.install_success
341+
if install_succeeded is not None and not install_succeeded and current_error is None:
325342
event = events.Event(
326343
name=events.EventType.INSTALL_PROGRESS_TEXT,
327344
data={"title": f"Failed to install {package_title}", "progress": 100},
328345
)
329346
events.post_event_sync(event, event_queue=events.gui_event_queue)
330-
return (False, retcode)
347+
logger.warning(
348+
"Stopping install - script reported success-(%s) and current error (%s)",
349+
install_succeeded,
350+
current_error,
351+
)
352+
return (False, install_succeeded)
353+
# Success return code recieved
354+
return (False, install_succeeded)
331355
# Not a matching line - continue
332356
return (True, None)
333357

@@ -343,23 +367,26 @@ def process_line(line, is_dependency: bool) -> ProgressResult | ErrorResult | Re
343367
except RecovorableRuntimeError as e:
344368
logger.info("Got a recoverable error: %s", e)
345369
if is_dependency and config.ALLOW_DEPENDENCIES_TO_FAIL_DUE_TO_NEWER_VERSION_INSTALLED:
370+
logger.info("Settings allow for success to be returned")
346371
# Fudge progress to say it's installed successfully if
347372
# we are happy to ignore the error as it's a dependency
348373
# and the error says that it's already installed.
349-
return ReturnCodeResult(0)
374+
return ReturnCodeResult(True)
350375
else:
376+
logger.warning("Settings insist this is a true failure")
351377
return ErrorResult(e)
352378
except RuntimeError as e:
353379
return ErrorResult(e)
354-
elif "RETCODE=" in line:
355-
return_code = parse_retcode(line)
356-
logger.info("Retcode found: %s", return_code)
357-
if return_code is not None:
358-
return ReturnCodeResult(return_code)
380+
elif "INSTALL_SUCCESS===" in line:
381+
install_succeeded = parse_retcode(line)
382+
if install_succeeded is not None:
383+
logger.info("Success state %s found from line", install_succeeded)
384+
return ReturnCodeResult(install_succeeded)
359385

360386

361387
class RecovorableRuntimeError(RuntimeError):
362388
"""Used when an error is raised but it needs to be parsed differently."""
389+
363390
pass
364391

365392

@@ -371,28 +398,36 @@ def parse_error(error_string: str):
371398
raise RuntimeError("The root certificate of the signature in the app package or bundle must be trusted.")
372399
elif "0x80073D06" in error_string:
373400
raise RecovorableRuntimeError("A newer version of this package is already installed!")
401+
elif "0x80073D02" in error_string:
402+
raise RecovorableRuntimeError("A conflicting application is open!")
374403
elif "Add-AppxProvisionedPackage : The requested operation requires elevation" in error_string:
375404
raise RuntimeError("The requested operation requires elevation")
376405
elif "ObjectNotFound" in error_string:
377406
raise RuntimeError("Installer file not found!")
378407
raise RuntimeError("Unknown error!")
379408

380409

381-
def parse_retcode(line: str) -> int:
410+
def parse_retcode(line: str) -> bool | None:
382411
"""Get the retcode out of a string.
383412
384413
Expects RETCODE=x where x is the retcode and any
385414
amount of values either side.
386415
"""
387-
split = line.split("RETCODE=")
388-
returncode = split[1][0]
416+
split = line.split("INSTALL_SUCCESS===")
417+
install_result = split[1][0]
418+
install_result_confirmation_tail = split[1][1]
389419
try:
420+
logger.info("Parsing return value %s from %s", install_result, split)
390421
# Line can sometimes be the command which gives an incorrect value
391422
# Such as ...ho\x1b[m RETCODE=\x1b[9...
392-
int_retcode = int(returncode)
423+
bool_success = bool(int(install_result))
424+
if install_result_confirmation_tail == "q":
425+
logger.debug("Line rejected, don't have expected tail.")
426+
return None
393427
except ValueError:
428+
logger.debug("Value is not a bool")
394429
return None
395-
return int_retcode
430+
return bool(bool_success)
396431

397432

398433
def progress_mincer(package_progress: int, packages_to_install: int, package_number: int) -> int:

uv.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)