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())