From eb5a2add9fa78140e376bb0c581ce5292b5bfb9d Mon Sep 17 00:00:00 2001 From: Christian <5859228+crysxd@users.noreply.github.com> Date: Sat, 25 Nov 2023 15:52:04 +0100 Subject: [PATCH] Add app instance management --- moonraker_octoapp/moonrakerappstorage.py | 27 ++++++++ moonraker_octoapp/moonrakerclient.py | 12 +++- moonraker_octoapp/moonrakerdatabase.py | 67 +++++++++++++++--- moonraker_octoapp/moonrakerhost.py | 11 +-- moonraker_octoapp/uiinjector.py | 14 ++-- octoapp/appsstorage.py | 88 ++++++------------------ octoapp/notificationsender.py | 20 +++--- octoprint_octoapp/octoprintappstorage.py | 16 ++--- 8 files changed, 142 insertions(+), 113 deletions(-) create mode 100644 moonraker_octoapp/moonrakerappstorage.py diff --git a/moonraker_octoapp/moonrakerappstorage.py b/moonraker_octoapp/moonrakerappstorage.py new file mode 100644 index 0000000..4187049 --- /dev/null +++ b/moonraker_octoapp/moonrakerappstorage.py @@ -0,0 +1,27 @@ +import time +from octoapp.sentry import Sentry +from .moonrakerdatabase import MoonrakerDatabase +from octoapp.appsstorage import AppInstance, AppStorageHelper + +class MoonrakerAppStorage: + + def __init__(self, database): + self.First = False + self.Database = database + + + # !! Platform Command Handler Interface Function !! + # + # This must return a list of AppInstance + # + def GetAllApps(self) -> [AppInstance]: + apps = self.Database.GetAppsEntry() + return list(map(lambda app: AppInstance.FromDict(app), apps)) + + # !! Platform Command Handler Interface Function !! + # + # This must receive a lsit of AppInstnace + # + def RemoveApps(self, apps:[AppInstance]): + apps = list(map(lambda app: app.FcmToken, apps)) + self.Database.RemoveAppEntries(apps) \ No newline at end of file diff --git a/moonraker_octoapp/moonrakerclient.py b/moonraker_octoapp/moonrakerclient.py index a7d7f24..d904350 100644 --- a/moonraker_octoapp/moonrakerclient.py +++ b/moonraker_octoapp/moonrakerclient.py @@ -63,8 +63,8 @@ class MoonrakerClient: @staticmethod - def Init(logger, isObserverMode:bool, moonrakerConfigFilePath:str, observerConfigPath:str, printerId, connectionStatusHandler, pluginVersionStr): - MoonrakerClient._Instance = MoonrakerClient(logger, isObserverMode, moonrakerConfigFilePath, observerConfigPath, printerId, connectionStatusHandler, pluginVersionStr) + def Init(logger, isObserverMode:bool, moonrakerConfigFilePath:str, observerConfigPath:str, printerId, connectionStatusHandler, pluginVersionStr, moonrakerDatabase): + MoonrakerClient._Instance = MoonrakerClient(logger, isObserverMode, moonrakerConfigFilePath, observerConfigPath, printerId, connectionStatusHandler, pluginVersionStr, moonrakerDatabase) @staticmethod @@ -72,7 +72,7 @@ def Get(): return MoonrakerClient._Instance - def __init__(self, logger:logging.Logger, isObserverMode:bool, moonrakerConfigFilePath:str, observerConfigPath:str, printerId:str, connectionStatusHandler, pluginVersionStr:str) -> None: + def __init__(self, logger:logging.Logger, isObserverMode:bool, moonrakerConfigFilePath:str, observerConfigPath:str, printerId:str, connectionStatusHandler, pluginVersionStr:str, moonrakerDatabase) -> None: self.Logger = logger self.IsObserverMode = isObserverMode self.MoonrakerConfigFilePath = moonrakerConfigFilePath @@ -81,6 +81,7 @@ def __init__(self, logger:logging.Logger, isObserverMode:bool, moonrakerConfigFi self.PrinterId = printerId self.ConnectionStatusHandler = connectionStatusHandler self.PluginVersionStr = pluginVersionStr + self.MoonrakerDatabase = moonrakerDatabase # Setup the json-rpc vars self.JsonRpcIdLock = threading.Lock() @@ -799,6 +800,11 @@ def OnPrintStart(self, fileName): # Only process notifications when ready, aka after state sync. if self.IsReadyToProcessNotifications is False: return + + # Get our name + name = MoonrakerClient.Get().MoonrakerDatabase.GetPrinterName() + self.NotificationHandler.NotificationSender.PrinterName = name + self.Logger.info("Printer is called %s" % name) # Since this is a new print, reset the cache. The file name might be the same as the last, but have # different props, so we will always reset. We know when we are printing the same file name will have the same props. diff --git a/moonraker_octoapp/moonrakerdatabase.py b/moonraker_octoapp/moonrakerdatabase.py index f9fbe00..742be36 100644 --- a/moonraker_octoapp/moonrakerdatabase.py +++ b/moonraker_octoapp/moonrakerdatabase.py @@ -2,6 +2,7 @@ import logging from octoapp.sentry import Sentry +from octoapp.appsstorage import AppInstance from .moonrakerclient import MoonrakerClient @@ -13,6 +14,63 @@ def __init__(self, logger:logging.Logger, printerId:str, pluginVersion:str) -> N self.PrinterId = printerId self.PluginVersion = pluginVersion + def GetAppsEntry(self): + self.Logger.info("Getting apps") + result = MoonrakerClient.Get().SendJsonRpcRequest("server.database.get_item", + { + "namespace": "octoapp", + "key": "apps", + }) + if result.GetErrorCode() == 404 or result.GetErrorCode() == -32601: + self.Logger.error("No apps set") + return [] + + if result.HasError(): + self.Logger.error("Ensure database entry item post failed. "+result.GetLoggingErrorStr()) + raise Exception("Unable to fetch apps: %s" % result.GetLoggingErrorStr()) + + out = [] + value = result.GetResult()["value"] + for key in value.keys(): out.append(value[key]) + return out + + def GetPrinterName(self) -> str: + mainsailResult = MoonrakerClient.Get().SendJsonRpcRequest("server.database.get_item", + { + "namespace": "mainsail", + "key": "general.printername", + }) + if mainsailResult.HasError() is False and mainsailResult.GetResult() is not None: + return mainsailResult.GetResult()["value"] + + fluiddResult = MoonrakerClient.Get().SendJsonRpcRequest("server.database.get_item", + { + "namespace": "fluidd", + "key": "uiSettings.general.instanceName", + }) + if fluiddResult.HasError() is False and mainsailResult.GetResult() is not None: + return mainsailResult.GetResult()["value"] + + if mainsailResult.HasError() is False & mainsailResult.GetErrorCode() != 404 or mainsailResult.GetErrorCode() != -3260: + self.Logger.error("Failed to load Mainsail printer name"+mainsailResult.GetLoggingErrorStr()) + + if fluiddResult.HasError() is False & fluiddResult.GetErrorCode() != 404 or fluiddResult.GetErrorCode() != -3260: + self.Logger.error("Failed to load Fluidd printer name"+fluiddResult.GetLoggingErrorStr()) + + return "Klipper" + + def RemoveAppEntries(self, apps: []): + self.Logger.info("Removing apps: %s" % apps) + + for appId in apps: + result = MoonrakerClient.Get().SendJsonRpcRequest("server.database.delete_item", + { + "namespace": "octoapp", + "key": "apps.%s" % appId, + }) + if result.HasError(): + self.Logger.error("Unable to remove app %s: %s" % (appId, result.GetLoggingErrorStr())) + def EnsureOctoAppDatabaseEntry(self): # Useful for debugging. @@ -21,15 +79,6 @@ def EnsureOctoAppDatabaseEntry(self): # We use a few database entries under our own name space to share information with apps and other plugins. # Note that since these are used by 3rd party systems, they must never change. We also use this for our frontend. result = MoonrakerClient.Get().SendJsonRpcRequest("server.database.post_item", - { - "namespace": "octoapp", - "key": "public.printerId", - "value": self.PrinterId - }) - if result.HasError(): - self.Logger.error("Ensure database entry item post failed. "+result.GetLoggingErrorStr()) - return - result = MoonrakerClient.Get().SendJsonRpcRequest("server.database.post_item", { "namespace": "octoapp", "key": "public.pluginVersion", diff --git a/moonraker_octoapp/moonrakerhost.py b/moonraker_octoapp/moonrakerhost.py index 9dfaf9a..00fae19 100644 --- a/moonraker_octoapp/moonrakerhost.py +++ b/moonraker_octoapp/moonrakerhost.py @@ -11,6 +11,7 @@ from octoapp.Proto.ServerHost import ServerHost from octoapp.localip import LocalIpHelper from octoapp.compat import Compat +from octoapp.appsstorage import AppStorageHelper from .config import Config from .secrets import Secrets @@ -28,6 +29,7 @@ from .filemetadatacache import FileMetadataCache from .uiinjector import UiInjector from .observerconfigfile import ObserverConfigFile +from .moonrakerappstorage import MoonrakerAppStorage # This file is the main host for the moonraker service. class MoonrakerHost: @@ -114,6 +116,10 @@ def RunBlocking(self, klipperConfigDir, isObserverMode, localStorageDir, service # Setup the database helper self.MoonrakerDatabase = MoonrakerDatabase(self.Logger, printerId, pluginVersionStr) + # Setup app storage + moonrakerAppStorage = MoonrakerAppStorage(self.MoonrakerDatabase) + AppStorageHelper.Init(moonrakerAppStorage) + # Setup the credential manager. MoonrakerCredentialManager.Init(self.Logger, moonrakerConfigFilePath, isObserverMode) @@ -146,7 +152,7 @@ def RunBlocking(self, klipperConfigDir, isObserverMode, localStorageDir, service # When everything is setup, start the moonraker client object. # This also creates the Notifications Handler and Gadget objects. # This doesn't start the moon raker connection, we don't do that until OE connects. - MoonrakerClient.Init(self.Logger, isObserverMode, moonrakerConfigFilePath, observerConfigFilePath, printerId, self, pluginVersionStr) + MoonrakerClient.Init(self.Logger, isObserverMode, moonrakerConfigFilePath, observerConfigFilePath, printerId, self, pluginVersionStr, self.MoonrakerDatabase) # Init our file meta data cache helper FileMetadataCache.Init(self.Logger, MoonrakerClient.Get()) @@ -167,9 +173,6 @@ def RunBlocking(self, klipperConfigDir, isObserverMode, localStorageDir, service # Now start the main runner! MoonrakerClient.Get().RunBlocking() - - while 1: - time.sleep(5) except Exception as e: Sentry.Exception("!! Exception thrown out of main host run function.", e) diff --git a/moonraker_octoapp/uiinjector.py b/moonraker_octoapp/uiinjector.py index 6c68751..0132669 100644 --- a/moonraker_octoapp/uiinjector.py +++ b/moonraker_octoapp/uiinjector.py @@ -151,13 +151,13 @@ def _DoInject(self, staticHtmlRootPath) -> bool: # Searches the text for our special tag. def _FindSpecialJsTagIndex(self, htmlLower) -> int: - c_jsTagSearch = "src=\"/oe/ui." + c_jsTagSearch = "src=\"/octoapp/ui." return htmlLower.find(c_jsTagSearch) # Searches the text for our special tag. def _FindSpecialCssTagIndex(self, htmlLower) -> int: - c_cssTagSearch = "href=\"/oe/ui." + c_cssTagSearch = "href=\"/octoapp/ui." return htmlLower.find(c_cssTagSearch) @@ -238,10 +238,10 @@ def _InjectIntoHtml(self, indexHtmlFilePath) -> bool: # We add some indents to re-create the about correct whitespace. # Note that since the update logic needs to find these file names, they can't change! # Especially the parts we search for, or there will be multiple tags showing up. - # "src=\"/oe/ui." - # "href=\"/oe/ui." - # The string "oe/ui.js?hash=" and "oe/ui.css?hash=" are important not to change. - tags = f"\r\n\r\n" + # "src=\"/octoapp/ui." + # "href=\"/octoapp/ui." + # The string "octoapp/ui.js?hash=" and "octoapp/ui.css?hash=" are important not to change. + tags = f"\r\n\r\n" # Inject the tags into the html htmlText = htmlText[:headEndTag] + tags + htmlText[headEndTag:] @@ -355,7 +355,7 @@ def _UpdateSwHash(self, staticHtmlRootPath) -> None: def _UpdateStaticFilesIntoRootIfNeeded(self, staticHtmlRootPath): try: # Ensure the dir exists. - oeStaticFileRoot = os.path.join(staticHtmlRootPath, "oe") + oeStaticFileRoot = os.path.join(staticHtmlRootPath, "octoapp") if os.path.exists(oeStaticFileRoot) is False: os.makedirs(oeStaticFileRoot) diff --git a/octoapp/appsstorage.py b/octoapp/appsstorage.py index 7d0e9e0..ce4f1d6 100644 --- a/octoapp/appsstorage.py +++ b/octoapp/appsstorage.py @@ -51,16 +51,16 @@ def ToDict(self): def FromDict(dict:dict): return AppInstance( fcmToken=dict["fcmToken"], - fcmFallbackToken=dict["fcmTokenFallback"], + fcmFallbackToken=dict.get("fcmTokenFallback", None), instanceId=dict["instanceId"], - displayName=dict["displayName"], - displayDescription=dict["displayDescription"], - model=dict["model"], - appVersion=dict["appVersion"], - appBuild=dict["appBuild"], - appLanguage=dict["appLanguage"], - lastSeenAt=dict["lastSeenAt"], - expireAt=dict["expireAt"], + displayName=dict.get("displayName", "Unknown"), + displayDescription=dict.get("displayDescription", ""), + model=dict.get("model", "Unknown"), + appVersion=dict.get("appVersion", "Unknown"), + appBuild=dict.get("appBuild", 1), + appLanguage=dict.get("appLanguage", "en"), + lastSeenAt=dict.get("lastSeenAt", 0), + expireAt=dict.get("expireAt", 0), ) @@ -81,61 +81,11 @@ def Get(): def __init__(self, appStoragePlatformHelper): self.AppStoragePlatformHelper = appStoragePlatformHelper - - def continuously_check_activities_expired(self): - t = threading.Thread( - target=self.do_continuously_check_activities_expired, - args=[] - ) - t.daemon = True - t.start() - - def do_continuously_check_activities_expired(self): - Sentry.Debug("APPS", "Checking for expired apps every 60s") - while True: - time.sleep(60) - - try: - expired = self.get_expired_apps(self.get_activities(self.get_apps())) - if len(expired): - Sentry.Debug("APPS", "Found %s expired apps" % len(expired)) - self.LogApps() - - expired_activities = self.GetActivities(expired) - if len(expired_activities): - # This will end the live activity, we currently do not send a notification to inform - # the user, we can do so by setting is_end=False and the apnsData as below - apnsData=self.create_activity_content_state( - is_end=True, - liveActivityState="expired", - state=self.print_state - ) - # apnsData["alert"] = { - # "title": "Updates paused for %s" % self.print_state.get("name", ""), - # "body": "Live activities expire after 8h, open OctoApp to renew" - # } - self.send_notification_blocking_raw( - targets=expired_activities, - high_priority=True, - apnsData=apnsData, - androidData="none" - ) - - filtered_apps = list(filter(lambda app: any(app.fcmToken != x.fcmToken for x in expired), self.get_apps())) - self.SetAllApps(filtered_apps) - self.LogApps() - Sentry.Debug("APPS", "Cleaned up expired apps") - - - except Exception as e: - Sentry.ExceptionNoSend("Failed to retire expired", e) - - def GetAndroidApps(self, apps): return list(filter(lambda app: not app.FcmToken.startswith("activity:") and not app.FcmToken.startswith("ios:"), apps)) def GetExpiredApps(self, apps): - return list(filter(lambda app: app.ExpiresAt is not None and time.time() > app.ExpiresAt, apps)) + return list(filter(lambda app: app.ExpireAt is not None and time.time() > app.ExpireAt, apps)) def GetIosApps(self, apps): return list(filter(lambda app: app.FcmToken.startswith("ios:"), apps)) @@ -147,25 +97,29 @@ def GetDefaultExpirationFromNow(self): return (time.time() + 2592000) def LogApps(self): - self.AppStoragePlatformHelper.LogAllApps() + apps = self.GetAllApps() + Sentry.Debug("APPS", "Now %s apps registered" % len(apps)) + for app in apps: + Sentry.Debug("APPS", " => %s" % app.FcmToken[0:100]) def RemoveTemporaryApps(self, for_instance_id=None): apps = self.GetAllApps() if for_instance_id is None: - apps = list(filter(lambda app: not app.FcmToken.startswith("activity:") ,apps)) + apps = list(filter(lambda app: app.FcmToken.startswith("activity:") ,apps)) Sentry.Debug("APPS", "Removed all temporary apps") else: - apps = list(filter(lambda app: not app.FcmToken.startswith("activity:") or app.instanceId != for_instance_id ,apps)) + apps = list(filter(lambda app: app.FcmToken.startswith("activity:") and app.instanceId == for_instance_id , apps)) Sentry.Debug("APPS", "Removed all temporary apps for %s" % for_instance_id) - self.SetAllApps(apps) + self.RemoveApps(apps) def GetAllApps(self) -> [AppInstance]: apps = self.AppStoragePlatformHelper.GetAllApps() Sentry.Debug("APPS", "Loading %s apps" % len(apps)) return apps - def SetAllApps(self, apps:[AppInstance]): - Sentry.Debug("APPS", "Storing %s apps" % len(apps)) - self.AppStoragePlatformHelper.SetAllApps(apps) \ No newline at end of file + def RemoveApps(self, apps: [AppInstance]): + Sentry.Debug("APPS", "Removing %s apps" % len(apps)) + self.AppStoragePlatformHelper.RemoveApps(apps) + self.LogApps() diff --git a/octoapp/notificationsender.py b/octoapp/notificationsender.py index 66ec5a5..45dc2c8 100644 --- a/octoapp/notificationsender.py +++ b/octoapp/notificationsender.py @@ -158,8 +158,8 @@ def _doSendNotification(self, targets, highProiroty, apnsData, androidData): invalid_tokens = r.json()["invalidTokens"] for fcmToken in invalid_tokens: Sentry.Info("SENDER", "Removing %s, no longer valid" % fcmToken) - apps = [app for app in apps if app.FcmToken != fcmToken] - AppStorageHelper.Get().SetAllApps(apps) + apps = [app for app in apps if app.FcmToken == fcmToken] + AppStorageHelper.Get().RemoveApps(apps) AppStorageHelper.Get().LogApps() except Exception as e: @@ -282,7 +282,7 @@ def _createApnsPushData(self, event, state): # Let's only end the activity on cancel. If we end it on completed the alert isn't shown data = self._createActivityContentState( - is_end=event == self.EVENT_CANCELLED, + isEnd=event == self.EVENT_CANCELLED, state=state, liveActivityState=liveActivityState ) @@ -305,9 +305,9 @@ def _createApnsPushData(self, event, state): return data - def _createActivityContentState(self, is_end, state, liveActivityState): + def _createActivityContentState(self, isEnd, state, liveActivityState): return { - "event": "end" if is_end else "update", + "event": "end" if isEnd else "update", "content-state": { "fileName": state.get("FileName", None), "progress":state.get("ProgressPercentage", None), @@ -381,7 +381,7 @@ def _doContinuouslyCheckActivitiesExpired(self): try: helper = AppStorageHelper.Get() - expired = helper.GetExpiredApps(helper.GetActivities(helper.GetAllApps())) + expired = helper.GetExpiredApps(helper.GetAllApps()) if len(expired): Sentry.Debug("SENDER", "Found %s expired apps" % len(expired)) helper.LogApps() @@ -389,9 +389,9 @@ def _doContinuouslyCheckActivitiesExpired(self): expired_activities = helper.GetActivities(expired) if len(expired_activities): # This will end the live activity, we currently do not send a notification to inform - # the user, we can do so by setting is_end=False and the apnsData as below + # the user, we can do so by setting isEnd=False and the apnsData as below apnsData=self._createActivityContentState( - is_end=True, + isEnd=True, liveActivityState="expired", state=self.LastPrintState ) @@ -406,9 +406,7 @@ def _doContinuouslyCheckActivitiesExpired(self): androidData="none" ) - filtered_apps = list(filter(lambda app: any(app.FcmToken != x.FcmToken for x in expired), helper.GetAllApps())) - helper.SetAllApps(filtered_apps) - helper.LogApps() + helper.RemoveApps(expired) Sentry.Debug("SENDER", "Cleaned up expired apps") diff --git a/octoprint_octoapp/octoprintappstorage.py b/octoprint_octoapp/octoprintappstorage.py index dfaf06c..137a418 100644 --- a/octoprint_octoapp/octoprintappstorage.py +++ b/octoprint_octoapp/octoprintappstorage.py @@ -53,6 +53,10 @@ def OnEvent(self, event, payload): # # This must receive a lsit of AppInstnace # + def RemoveApps(self, apps:[AppInstance]): + filtered_apps = list(filter(lambda app: any(app.FcmToken != x.FcmToken for x in apps), self.GetAllApps())) + self.SetAllApps(filtered_apps) + def SetAllApps(self, apps:[AppInstance]): mapped_apps = list(map(lambda x: x.ToDict(), apps)) @@ -62,17 +66,6 @@ def SetAllApps(self, apps:[AppInstance]): self.SendSettingsPluginMessage(apps) - # !! Platform Command Handler Interface Function !! - # - # This must receive a lsit of AppInstnace - # - def LogAllApps(self): - apps = self.GetAllApps() - Sentry.Debug("APPS", "Now %s apps registered" % len(apps)) - for app in apps: - Sentry.Debug("APPS", " => %s" % app.FcmToken[0:100]) - - def UpgradeDataStructure(self): try: if not os.path.isfile(self.DataFile): @@ -150,7 +143,6 @@ def OnApiCommand(self, command, data): # save Sentry.Info("NOTIFICATION", "Registered app %s" % fcmToken) self.SetAllApps(apps) - AppStorageHelper.Get().LogApps() self.parent._settings.save() return flask.jsonify(dict())