diff --git a/py_installer/Installer.py b/py_installer/Installer.py index 9288af4..1e033cf 100644 --- a/py_installer/Installer.py +++ b/py_installer/Installer.py @@ -114,6 +114,9 @@ def _RunInternal(self): uninstall.DoUninstall(context) return + # Since this runs an async thread, kick it off now so it can start working. + zstdInstallThreadOrNone = ZStandard.TryToInstallZStandardAsync(context) + # Next step is to discover and fill out the moonraker config file path and service file name. # If we are doing an companion or bambu setup, we need the user to help us input the details to the external moonraker IP or bambu printer. # This is the hardest part of the setup, because it's highly dependent on the system and different moonraker setups. @@ -145,9 +148,6 @@ def _RunInternal(self): # Installing ffmpeg is best effort and not required for the plugin to work. Ffmpeg.TryToInstallFfmpeg(context) - # We also want to try to install the optional zstandard lib for compression. - ZStandard.TryToInstallZStandard(context) - # Before we start the service, check if the secrets config file already exists and if a printer id already exists. # This will indicate if this is a fresh install or not. context.ExistingPrinterId = Linker.GetPrinterIdFromServiceSecretsConfigFile(context) @@ -174,6 +174,15 @@ def _RunInternal(self): linker = Linker() linker.Run(context) + # Wait for the install thread to complete or timeout. + # If we fail to wait for it, the plugin runtime will try to install zstandard as well, so nbd. + if zstdInstallThreadOrNone is not None: + Logger.Info("Finishing up... this might take a moment...") + try: + zstdInstallThreadOrNone.join(timeout=10.0) + except Exception as e: + Logger.Debug(f"Failed to join ztd installer thread. {str(e)}") + # Success! Logger.Blank() Logger.Blank() diff --git a/py_installer/Updater.py b/py_installer/Updater.py index c7aa27b..2f800e9 100644 --- a/py_installer/Updater.py +++ b/py_installer/Updater.py @@ -31,6 +31,9 @@ class Updater: def DoUpdate(self, context:Context): Logger.Header("Starting Update Logic") + # Since this takes a while, kick it off now. The pip install can take upwards of 30 seconds. + zstdInstallThreadOrNone = ZStandard.TryToInstallZStandardAsync(context) + # Enumerate all service file to find any local plugins, Sonic Pad plugins, companion service files, and bambu service files, since all service files contain this name. # Note GetServiceFileFolderPath will return dynamically based on the OsType detected. # Use sorted, so the results are in a nice user presentable order. @@ -49,9 +52,6 @@ def DoUpdate(self, context:Context): # On any system, try to install or update ffmpeg. Ffmpeg.TryToInstallFfmpeg(context) - # We also want to try to install or update the optional zstandard lib for compression. - ZStandard.TryToInstallZStandard(context) - Logger.Info("We found the following plugins to update:") for s in foundOeServices: Logger.Info(f" {s}") @@ -82,6 +82,15 @@ def DoUpdate(self, context:Context): # Try to update the crontab job if needed self.EnsureCronUpdateJob(context.RepoRootFolder) + # Wait for the install thread to complete or timeout. + # If we fail to wait for it, the plugin runtime will try to install zstandard as well, so nbd. + if zstdInstallThreadOrNone is not None: + Logger.Info("Finishing up... this might take a moment...") + try: + zstdInstallThreadOrNone.join(timeout=30.0) + except Exception as e: + Logger.Debug(f"Failed to join ztd installer thread. {str(e)}") + Logger.Blank() Logger.Header("-------------------------------------------") Logger.Info( " OctoEverywhere Update Successful") diff --git a/py_installer/ZStandard.py b/py_installer/ZStandard.py index bfb879a..9c296d2 100644 --- a/py_installer/ZStandard.py +++ b/py_installer/ZStandard.py @@ -1,6 +1,7 @@ import sys import time import subprocess +import threading import multiprocessing from octoeverywhere.compression import Compression @@ -13,36 +14,49 @@ class ZStandard: # Tries to install zstandard, but this won't fail if the install fails. + # The PIP install can take quite a long time (20-30 seconds) so we run in async. + # Returns None if no work will be done. Otherwise it returns a thread. @staticmethod - def TryToInstallZStandard(context:Context): + def TryToInstallZStandardAsync(context:Context) -> threading.Thread: # We don't even try installing on K1 or SonicPad, we know it fail. if context.OsType == OsTypes.K1 or context.OsType == OsTypes.SonicPad: - return + return None # We also don't try install on systems with 2 cores or less, since it's too much work and the OS most of the time # Can't support zstandard because there's no pre-made binary, it can't be built, and the install process will take too long. if multiprocessing.cpu_count() < Compression.ZStandardMinCoreCountForInstall: - return - - # Try to install the system package, if possible. This might bring in a binary. - # If this fails, the PY package might be able to still bring in a pre-built binary. - Logger.Info("Installing zstandard, this might take a moment...") - startSec = time.time() - (returnCode, stdOut, stdError) = Util.RunShellCommand("sudo apt-get install zstd -y", False) - Logger.Debug(f"Zstandard apt install result. Code: {returnCode}, StdOut: {stdOut}, StdErr: {stdError}") - - # Now try to install the PY package. - # NOTE: Use the same logic as we do in the Compression class. - # Only allow blocking up to 20 seconds, so we don't hang the installer too long. - result = subprocess.run([sys.executable, '-m', 'pip', 'install', Compression.ZStandardPipPackageString], timeout=30.0, check=False, capture_output=True) - Logger.Debug(f"Zstandard PIP install result. Code: {result.returncode}, StdOut: {result.stdout}, StdErr: {result.stderr}") - - # Report the status to the installer log. - if result.returncode == 0: - Logger.Info(f"zStandard successfully installed/updated. It took {str(round(time.time()-startSec, 2))} seconds.") - return - - # Tell the user, but this is a best effort, so if it fails we don't care. - # Any user who wants to use RTSP and doesn't have ffmpeg installed can use our help docs to install it. - Logger.Info(f"We didn't install zstandard. It took {str(round(time.time()-startSec, 2))} seconds. Output: {result.stderr}") + return None + + # Since the pip install can take a long time, do the install process async. + t = threading.Thread(target=ZStandard._InstallThread, daemon=True) + t.start() + return t + + + @staticmethod + def _InstallThread() -> None: + try: + # Try to install the system package, if possible. This might bring in a binary. + # If this fails, the PY package might be able to still bring in a pre-built binary. + Logger.Debug("Installing zstandard, this might take a moment...") + startSec = time.time() + (returnCode, stdOut, stdError) = Util.RunShellCommand("sudo apt-get install zstd -y", False) + Logger.Debug(f"Zstandard apt install result. Code: {returnCode}, StdOut: {stdOut}, StdErr: {stdError}") + + # Now try to install the PY package. + # NOTE: Use the same logic as we do in the Compression class. + # Only allow blocking up to 20 seconds, so we don't hang the installer too long. + result = subprocess.run([sys.executable, '-m', 'pip', 'install', Compression.ZStandardPipPackageString], timeout=30.0, check=False, capture_output=True) + Logger.Debug(f"Zstandard PIP install result. Code: {result.returncode}, StdOut: {result.stdout}, StdErr: {result.stderr}") + + # Report the status to the installer log. + if result.returncode == 0: + Logger.Debug(f"zStandard successfully installed/updated. It took {str(round(time.time()-startSec, 2))} seconds.") + return + + # Tell the user, but this is a best effort, so if it fails we don't care. + # Any user who wants to use RTSP and doesn't have ffmpeg installed can use our help docs to install it. + Logger.Debug(f"We didn't install zstandard. It took {str(round(time.time()-startSec, 2))} seconds. Output: {result.stderr}") + except Exception as e: + Logger.Debug(f"Error installing zstandard. {str(e)}")