diff --git a/bambu_octoapp/bambuclient.py b/bambu_octoapp/bambuclient.py index fa43570..2cdddc2 100644 --- a/bambu_octoapp/bambuclient.py +++ b/bambu_octoapp/bambuclient.py @@ -2,6 +2,7 @@ import time import json import socket +import logging import threading from typing import Any, Dict, List, Optional @@ -34,7 +35,7 @@ class BambuClient: _Instance:"BambuClient" = None #pyright: ignore[reportAssignmentType] # Useful for debugging. - _PrintMQTTMessages = True + _PrintMQTTMessages = False @staticmethod def Init(logger:LoggerLike, config:Config, stateTranslator:IBambuStateTranslator) -> None: @@ -75,6 +76,8 @@ def __init__(self, logger:LoggerLike, config:Config, stateTranslator:IBambuState # We use this var to keep track of consecutively failed connections self.ConsecutivelyFailedConnectionAttempts = 0 + # This flag indicates if we have tried a network scan since the plugin started. If not, we should do it again. + self.HasDoneNetScanSincePluginStart = False # Start a thread to setup and maintain the connection. self.CurrentConnectionContext:Optional[ConnectionContext] = None @@ -287,7 +290,15 @@ def _OnDisconnect(self, client:Any, userdata:Any, disconnect_flags:Any, reason_c # So if we were pending a subscribe call, give the user a better error message so they know the likely cause. if self.IsPendingSubscribe: self.Logger.error("Bambu printer mqtt connection lost when trying to sub for events.") - self.Logger.error(f"THIS USUALLY MEANS THE PRINTER SERIAL NUMBER IS WRONG. We tried to use the serial number '{self.PrinterSn}'. Double check the SN is correct.") + self.Logger.error("") + self.Logger.error("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + self.Logger.error("This might indicate the printer ACCESS CODE - OR - SERIAL NUMBER IS WRONG.") + self.Logger.error(f" Current Serial Number: '{self.PrinterSn}'") + self.Logger.error(f" Current Access Code: '{self.LanAccessCode}'") + self.Logger.error("") + self.Logger.error("Check these values match your printer. If they changed, run the OctoApp installer again to update them or update your Docker configuration.") + self.Logger.error("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + self.Logger.error("") else: self.Logger.warning("Bambu printer connection lost. We will try to reconnect in a few seconds.") # Clear the state since we lost the connection and won't stay synced. @@ -342,7 +353,7 @@ def _OnMessage(self, client:Any, userdata:Any, mqttMsg:mqtt.MQTTMessage) -> None raise Exception("Parsed json MQTT message returned None") # Print for debugging if desired. - if BambuClient._PrintMQTTMessages: + if BambuClient._PrintMQTTMessages and self.Logger.isEnabledFor(logging.DEBUG): self.Logger.debug("Incoming Bambu Message:\r\n"+json.dumps(msg, indent=3)) # Since we keep a track of the state locally from the partial updates, we need to feed all updates to our state object. @@ -433,7 +444,8 @@ def _GetConnectionContextToTry(self, isConnectAttemptFromEventBump:bool) -> Conn if isConnectAttemptFromEventBump is False: self.ConsecutivelyFailedConnectionAttempts += 1 doPrinterSearch = False - if self.ConsecutivelyFailedConnectionAttempts > 6: + # We only search every now and then, unless this is one of the first connect attempts after the plugin started. + if (self.HasDoneNetScanSincePluginStart is False and self.ConsecutivelyFailedConnectionAttempts > 1) or self.ConsecutivelyFailedConnectionAttempts > 6: self.ConsecutivelyFailedConnectionAttempts = 0 doPrinterSearch = True @@ -468,7 +480,8 @@ def _GetConnectionContextToTry(self, isConnectAttemptFromEventBump:bool) -> Conn self.Logger.info(f"Searching for your Bambu Lab printer {self.PrinterSn}") if self.LanAccessCode is None: return self._GetLocalConnectionContext(configIpOrHostname) - ips = NetworkSearch.ScanForInstances_Bambu(self.Logger, self.LanAccessCode, self.PrinterSn, threadCount=25, delaySec=0.2) + self.HasDoneNetScanSincePluginStart = True + ips = NetworkSearch.ScanForInstances_Bambu(self.Logger, self.LanAccessCode, self.PrinterSn, ipHint=configIpOrHostname, threadCount=25, delaySec=0.2) # If we get an IP back, it is the printer. # The scan above will only return an IP if the printer was successfully connected to, logged into, and fully authorized with the Access Token and Printer SN. diff --git a/bambu_octoapp/bambumodels.py b/bambu_octoapp/bambumodels.py index 86e3e32..1960a86 100644 --- a/bambu_octoapp/bambumodels.py +++ b/bambu_octoapp/bambumodels.py @@ -26,6 +26,7 @@ class BambuHmsEntry(NamedTuple): class BambuPrintErrors(Enum): Unknown = 1 # This will be most errors, since most of them aren't mapped FilamentRunOut = 2 + PrintFailureDetected = 3 # The Bambu AI detected a failure. # Since MQTT syncs a full state and then sends partial updates, we keep track of the full state diff --git a/bambu_octoapp/bambustatetranslater.py b/bambu_octoapp/bambustatetranslater.py index baf996c..4e72a23 100644 --- a/bambu_octoapp/bambustatetranslater.py +++ b/bambu_octoapp/bambustatetranslater.py @@ -156,8 +156,11 @@ def BambuOnPauseOrTempError(self, bambuState:BambuState) -> None: self.NotificationsHandler.OnFilamentChange() return - # Send a generic error. - self.NotificationsHandler.OnUserInteractionNeeded() + # Send the error string from the bambu API map. + errorStr = bambuState.GetFileNameWithNoExtension() + if errorStr is None: + errorStr = "General Error" + self.NotificationsHandler.OnError(errorStr) def BambuOnResume(self, bambuState:BambuState) -> None: diff --git a/elegoo_octoapp/elegoostatetranslater.py b/elegoo_octoapp/elegoostatetranslater.py index 2303f03..9513e47 100644 --- a/elegoo_octoapp/elegoostatetranslater.py +++ b/elegoo_octoapp/elegoostatetranslater.py @@ -1,8 +1,10 @@ +import threading from typing import Optional, Tuple from octoapp.logging import LoggerLike from octoapp.notificationshandler import NotificationsHandler from octoapp.interfaces import IPrinterStateReporter +from octoapp.util.delayedcallback import DelayedCallback from .elegooclient import ElegooClient from .elegoomodels import PrinterState @@ -14,11 +16,16 @@ # and to act as the printer state interface for Bambu printers. class ElegooStateTranslator(IPrinterStateReporter, IStateTranslator): + # The amount of time we will wait for a reconnect before we fire the disconnected notification. + c_ConnectionLostNotificationDelaySec = 10.0 + def __init__(self, logger:LoggerLike) -> None: self.Logger = logger self.NotificationsHandler:NotificationsHandler = None #pyright: ignore[reportAttributeAccessIssue] self.LastStatus:Optional[str] = None self.IsWaitingOnPrintInfoToFirePrintStart = False + self.DelayedConnectionLostCallback:Optional[DelayedCallback] = None + self.DelayedConnectionLostCallbackLock = threading.Lock() def SetNotificationHandler(self, notificationHandler:NotificationsHandler): @@ -32,18 +39,33 @@ def OnConnectionLost(self, wasFullyConnected:bool) -> None: # If we were fully connected and were printing or warming up, then report the connection loss. # Otherwise, don't bother, since it might just be the user turning off the printer. if wasFullyConnected and (PrinterState.IsPrepareOrSlicingState(self.LastStatus) or PrinterState.IsPrintingState(self.LastStatus, False)): - self.NotificationsHandler.OnError("Connection to printer lost during a print.") + # There seems to be a bug while printing where we will get disconnected from the printer's server but then reconnected with no effect on the print. + # To combat spammy notifications, we use a delayed callback to only fire after we know we have been disconnected for some time. + with self.DelayedConnectionLostCallbackLock: + if self.DelayedConnectionLostCallback is not None: + self.DelayedConnectionLostCallback.Cancel() + self.DelayedConnectionLostCallback = DelayedCallback.Create(self.Logger, "ElegooDelayedConnectionLostCallback", self.c_ConnectionLostNotificationDelaySec, self._DelayedConnectionLostCallback) # Always reset our state. self.LastStatus = None self.IsWaitingOnPrintInfoToFirePrintStart = False + def _DelayedConnectionLostCallback(self): + self.NotificationsHandler.OnError("Connection to printer lost during a print.") + + # Fired when any mqtt message comes in. # State will always be NOT NONE, since it's going to be created before this call. # The isFirstFullSyncResponse flag indicates if this is the first full state sync of a new connection. def OnStatusUpdate(self, pState:PrinterState, isFirstFullSyncResponse:bool) -> None: + # Ensure if we have a connection lost delay in progress, cancel it since we're back. + with self.DelayedConnectionLostCallbackLock: + if self.DelayedConnectionLostCallback is not None: + self.DelayedConnectionLostCallback.Cancel() + self.DelayedConnectionLostCallback = None + # First, if we have a new connection and we just synced, make sure the notification handler is in sync. if isFirstFullSyncResponse: self.NotificationsHandler.OnRestorePrintIfNeeded(pState.IsPrinting(False), pState.IsPaused(), pState.GetPrintCookie()) diff --git a/moonraker_octoapp/moonrakerclient.py b/moonraker_octoapp/moonrakerclient.py index 2bc08f6..ea3f142 100644 --- a/moonraker_octoapp/moonrakerclient.py +++ b/moonraker_octoapp/moonrakerclient.py @@ -388,8 +388,12 @@ def _OnWsNonResponseMessage(self, msg:Dict[str, Any]) -> None: jobObj = jobContainerObj["job"] filename = jobObj.get("filename", None) if filename is not None: - self.MoonrakerCompat.OnPrintStart(filename) - self.DownloadFileForProcessing(filename) + # Note sometimes the filename can just be "" + if len(filename) == 0: + self.Logger.info("Moonraker client detected print start with no file name, so we aren't firing the print started event.") + else: + self.MoonrakerCompat.OnPrintStart(filename) + self.DownloadFileForProcessing(filename) return elif action == "finished": # This can be a finish canceled or failed. @@ -560,6 +564,7 @@ def RunBlocking(self) -> None: onWsClose=self._onWsClose, onWsError=self._onWsError ) + self.WebSocket.SetDisableCertCheck(True) # Run until the socket closes # When it returns, ensure it's closed. diff --git a/octoapp/Proto/HandshakeAck.py b/octoapp/Proto/HandshakeAck.py new file mode 100644 index 0000000..01139f6 --- /dev/null +++ b/octoapp/Proto/HandshakeAck.py @@ -0,0 +1,159 @@ +# automatically generated by the FlatBuffers compiler, do not modify + +# namespace: Proto + +import octoflatbuffers +from typing import Any +from typing import Optional +class HandshakeAck(object): + __slots__ = ['_tab'] + + @classmethod + def GetRootAs(cls, buf, offset: int = 0): + n = octoflatbuffers.encode.Get(octoflatbuffers.packer.uoffset, buf, offset) + x = HandshakeAck() + x.Init(buf, n + offset) + return x + + @classmethod + def GetRootAsHandshakeAck(cls, buf, offset=0): + """This method is deprecated. Please switch to GetRootAs.""" + return cls.GetRootAs(buf, offset) + # HandshakeAck + def Init(self, buf: bytes, pos: int): + self._tab = octoflatbuffers.table.Table(buf, pos) + + # HandshakeAck + def Accepted(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # HandshakeAck + def ConnectedAccounts(self, j: int): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + if o != 0: + a = self._tab.Vector(o) + return self._tab.String(a + octoflatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) + return "" + + # HandshakeAck + def ConnectedAccountsLength(self) -> int: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + if o != 0: + return self._tab.VectorLen(o) + return 0 + + # HandshakeAck + def ConnectedAccountsIsNone(self) -> bool: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + return o == 0 + + # HandshakeAck + def Error(self) -> Optional[str]: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # HandshakeAck + def BackoffSeconds(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Uint64Flags, o + self._tab.Pos) + return 0 + + # HandshakeAck + def RequiresPluginUpdate(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # HandshakeAck + def Octokey(self) -> Optional[str]: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # HandshakeAck + def RsaChallengeResult(self) -> Optional[str]: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # HandshakeAck + def RequiresRekey(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + +def HandshakeAckStart(builder: octoflatbuffers.Builder): + builder.StartObject(8) + +def Start(builder: octoflatbuffers.Builder): + HandshakeAckStart(builder) + +def HandshakeAckAddAccepted(builder: octoflatbuffers.Builder, accepted: bool): + builder.PrependBoolSlot(0, accepted, 0) + +def AddAccepted(builder: octoflatbuffers.Builder, accepted: bool): + HandshakeAckAddAccepted(builder, accepted) + +def HandshakeAckAddConnectedAccounts(builder: octoflatbuffers.Builder, connectedAccounts: int): + builder.PrependUOffsetTRelativeSlot(1, octoflatbuffers.number_types.UOffsetTFlags.py_type(connectedAccounts), 0) + +def AddConnectedAccounts(builder: octoflatbuffers.Builder, connectedAccounts: int): + HandshakeAckAddConnectedAccounts(builder, connectedAccounts) + +def HandshakeAckStartConnectedAccountsVector(builder, numElems: int) -> int: + return builder.StartVector(4, numElems, 4) + +def StartConnectedAccountsVector(builder, numElems: int) -> int: + return HandshakeAckStartConnectedAccountsVector(builder, numElems) + +def HandshakeAckAddError(builder: octoflatbuffers.Builder, error: int): + builder.PrependUOffsetTRelativeSlot(2, octoflatbuffers.number_types.UOffsetTFlags.py_type(error), 0) + +def AddError(builder: octoflatbuffers.Builder, error: int): + HandshakeAckAddError(builder, error) + +def HandshakeAckAddBackoffSeconds(builder: octoflatbuffers.Builder, backoffSeconds: int): + builder.PrependUint64Slot(3, backoffSeconds, 0) + +def AddBackoffSeconds(builder: octoflatbuffers.Builder, backoffSeconds: int): + HandshakeAckAddBackoffSeconds(builder, backoffSeconds) + +def HandshakeAckAddRequiresPluginUpdate(builder: octoflatbuffers.Builder, requiresPluginUpdate: bool): + builder.PrependBoolSlot(4, requiresPluginUpdate, 0) + +def AddRequiresPluginUpdate(builder: octoflatbuffers.Builder, requiresPluginUpdate: bool): + HandshakeAckAddRequiresPluginUpdate(builder, requiresPluginUpdate) + +def HandshakeAckAddOctokey(builder: octoflatbuffers.Builder, octokey: int): + builder.PrependUOffsetTRelativeSlot(5, octoflatbuffers.number_types.UOffsetTFlags.py_type(octokey), 0) + +def AddOctokey(builder: octoflatbuffers.Builder, octokey: int): + HandshakeAckAddOctokey(builder, octokey) + +def HandshakeAckAddRsaChallengeResult(builder: octoflatbuffers.Builder, rsaChallengeResult: int): + builder.PrependUOffsetTRelativeSlot(6, octoflatbuffers.number_types.UOffsetTFlags.py_type(rsaChallengeResult), 0) + +def AddRsaChallengeResult(builder: octoflatbuffers.Builder, rsaChallengeResult: int): + HandshakeAckAddRsaChallengeResult(builder, rsaChallengeResult) + +def HandshakeAckAddRequiresRekey(builder: octoflatbuffers.Builder, requiresRekey: bool): + builder.PrependBoolSlot(7, requiresRekey, 0) + +def AddRequiresRekey(builder: octoflatbuffers.Builder, requiresRekey: bool): + HandshakeAckAddRequiresRekey(builder, requiresRekey) + +def HandshakeAckEnd(builder: octoflatbuffers.Builder) -> int: + return builder.EndObject() + +def End(builder: octoflatbuffers.Builder) -> int: + return HandshakeAckEnd(builder) diff --git a/octoapp/Proto/HandshakeSyn.py b/octoapp/Proto/HandshakeSyn.py index 5b7ad1f..01fd85b 100644 --- a/octoapp/Proto/HandshakeSyn.py +++ b/octoapp/Proto/HandshakeSyn.py @@ -176,8 +176,15 @@ def DeviceId(self) -> Optional[str]: return self._tab.String(o + self._tab.Pos) return None + # HandshakeSyn + def IsDockerContainer(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(40)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + def HandshakeSynStart(builder: octoflatbuffers.Builder): - builder.StartObject(18) + builder.StartObject(19) def Start(builder: octoflatbuffers.Builder): HandshakeSynStart(builder) @@ -296,6 +303,12 @@ def HandshakeSynAddDeviceId(builder: octoflatbuffers.Builder, deviceId: int): def AddDeviceId(builder: octoflatbuffers.Builder, deviceId: int): HandshakeSynAddDeviceId(builder, deviceId) +def HandshakeSynAddIsDockerContainer(builder: octoflatbuffers.Builder, isDockerContainer: bool): + builder.PrependBoolSlot(18, isDockerContainer, 0) + +def AddIsDockerContainer(builder: octoflatbuffers.Builder, isDockerContainer: bool): + HandshakeSynAddIsDockerContainer(builder, isDockerContainer) + def HandshakeSynEnd(builder: octoflatbuffers.Builder) -> int: return builder.EndObject() diff --git a/octoapp/Proto/OctoNotification.py b/octoapp/Proto/OctoNotification.py new file mode 100644 index 0000000..3b0c5dc --- /dev/null +++ b/octoapp/Proto/OctoNotification.py @@ -0,0 +1,127 @@ +# automatically generated by the FlatBuffers compiler, do not modify + +# namespace: Proto + +import octoflatbuffers +from typing import Any +from typing import Optional +class OctoNotification(object): + __slots__ = ['_tab'] + + @classmethod + def GetRootAs(cls, buf, offset: int = 0): + n = octoflatbuffers.encode.Get(octoflatbuffers.packer.uoffset, buf, offset) + x = OctoNotification() + x.Init(buf, n + offset) + return x + + @classmethod + def GetRootAsOctoNotification(cls, buf, offset=0): + """This method is deprecated. Please switch to GetRootAs.""" + return cls.GetRootAs(buf, offset) + # OctoNotification + def Init(self, buf: bytes, pos: int): + self._tab = octoflatbuffers.table.Table(buf, pos) + + # OctoNotification + def Title(self) -> Optional[str]: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # OctoNotification + def Text(self) -> Optional[str]: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # OctoNotification + def Type(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 0 + + # OctoNotification + def ActionText(self) -> Optional[str]: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # OctoNotification + def ActionLink(self) -> Optional[str]: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # OctoNotification + def ShowForSec(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 5 + + # OctoNotification + def ShowOnlyIfLoadedFromOe(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return True + +def OctoNotificationStart(builder: octoflatbuffers.Builder): + builder.StartObject(8) + +def Start(builder: octoflatbuffers.Builder): + OctoNotificationStart(builder) + +def OctoNotificationAddTitle(builder: octoflatbuffers.Builder, title: int): + builder.PrependUOffsetTRelativeSlot(0, octoflatbuffers.number_types.UOffsetTFlags.py_type(title), 0) + +def AddTitle(builder: octoflatbuffers.Builder, title: int): + OctoNotificationAddTitle(builder, title) + +def OctoNotificationAddText(builder: octoflatbuffers.Builder, text: int): + builder.PrependUOffsetTRelativeSlot(1, octoflatbuffers.number_types.UOffsetTFlags.py_type(text), 0) + +def AddText(builder: octoflatbuffers.Builder, text: int): + OctoNotificationAddText(builder, text) + +def OctoNotificationAddType(builder: octoflatbuffers.Builder, type: int): + builder.PrependInt8Slot(2, type, 0) + +def AddType(builder: octoflatbuffers.Builder, type: int): + OctoNotificationAddType(builder, type) + +def OctoNotificationAddActionText(builder: octoflatbuffers.Builder, actionText: int): + builder.PrependUOffsetTRelativeSlot(4, octoflatbuffers.number_types.UOffsetTFlags.py_type(actionText), 0) + +def AddActionText(builder: octoflatbuffers.Builder, actionText: int): + OctoNotificationAddActionText(builder, actionText) + +def OctoNotificationAddActionLink(builder: octoflatbuffers.Builder, actionLink: int): + builder.PrependUOffsetTRelativeSlot(5, octoflatbuffers.number_types.UOffsetTFlags.py_type(actionLink), 0) + +def AddActionLink(builder: octoflatbuffers.Builder, actionLink: int): + OctoNotificationAddActionLink(builder, actionLink) + +def OctoNotificationAddShowForSec(builder: octoflatbuffers.Builder, showForSec: int): + builder.PrependUint32Slot(6, showForSec, 5) + +def AddShowForSec(builder: octoflatbuffers.Builder, showForSec: int): + OctoNotificationAddShowForSec(builder, showForSec) + +def OctoNotificationAddShowOnlyIfLoadedFromOe(builder: octoflatbuffers.Builder, showOnlyIfLoadedFromOe: bool): + builder.PrependBoolSlot(7, showOnlyIfLoadedFromOe, 1) + +def AddShowOnlyIfLoadedFromOe(builder: octoflatbuffers.Builder, showOnlyIfLoadedFromOe: bool): + OctoNotificationAddShowOnlyIfLoadedFromOe(builder, showOnlyIfLoadedFromOe) + +def OctoNotificationEnd(builder: octoflatbuffers.Builder) -> int: + return builder.EndObject() + +def End(builder: octoflatbuffers.Builder) -> int: + return OctoNotificationEnd(builder) diff --git a/octoapp/Proto/OctoNotificationTypes.py b/octoapp/Proto/OctoNotificationTypes.py new file mode 100644 index 0000000..1a581b3 --- /dev/null +++ b/octoapp/Proto/OctoNotificationTypes.py @@ -0,0 +1,9 @@ +# automatically generated by the FlatBuffers compiler, do not modify + +# namespace: Proto + +class OctoNotificationTypes(object): + Notice = 0 + Info = 1 + Success = 2 + Error = 3 diff --git a/octoapp/Proto/WebStreamMsg.py b/octoapp/Proto/WebStreamMsg.py new file mode 100644 index 0000000..378f788 --- /dev/null +++ b/octoapp/Proto/WebStreamMsg.py @@ -0,0 +1,307 @@ +# automatically generated by the FlatBuffers compiler, do not modify + +# namespace: Proto + +import octoflatbuffers +from typing import Any +from octoapp.Proto.HttpInitialContext import HttpInitialContext +from typing import Optional +class WebStreamMsg(object): + __slots__ = ['_tab'] + + @classmethod + def GetRootAs(cls, buf, offset: int = 0): + n = octoflatbuffers.encode.Get(octoflatbuffers.packer.uoffset, buf, offset) + x = WebStreamMsg() + x.Init(buf, n + offset) + return x + + @classmethod + def GetRootAsWebStreamMsg(cls, buf, offset=0): + """This method is deprecated. Please switch to GetRootAs.""" + return cls.GetRootAs(buf, offset) + # WebStreamMsg + def Init(self, buf: bytes, pos: int): + self._tab = octoflatbuffers.table.Table(buf, pos) + + # WebStreamMsg + def StreamId(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 0 + + # WebStreamMsg + def IsOpenMsg(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # WebStreamMsg + def IsCloseMsg(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # WebStreamMsg + def IsDataTransmissionDone(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # WebStreamMsg + def IsControlFlagsOnly(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return True + + # WebStreamMsg + def FullStreamDataSize(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Int64Flags, o + self._tab.Pos) + return -1 + + # WebStreamMsg + def Data(self, j: int): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) + if o != 0: + a = self._tab.Vector(o) + return self._tab.Get(octoflatbuffers.number_types.Uint8Flags, a + octoflatbuffers.number_types.UOffsetTFlags.py_type(j * 1)) + return 0 + + # WebStreamMsg + def DataAsNumpy(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) + if o != 0: + return self._tab.GetVectorAsNumpy(octoflatbuffers.number_types.Uint8Flags, o) + return 0 + + # WebStreamMsg + def DataAsByteArray(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) + if o != 0: + return self._tab.GetVectorAsByteArray(o) + return 0 + + # WebStreamMsg + def DataLength(self) -> int: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) + if o != 0: + return self._tab.VectorLen(o) + return 0 + + # WebStreamMsg + def DataIsNone(self) -> bool: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) + return o == 0 + + # WebStreamMsg + def DataCompression(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 0 + + # WebStreamMsg + def OriginalDataSize(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Uint64Flags, o + self._tab.Pos) + return 0 + + # WebStreamMsg + def HttpInitialContext(self) -> Optional[HttpInitialContext]: + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(22)) + if o != 0: + x = self._tab.Indirect(o + self._tab.Pos) + obj = HttpInitialContext() + obj.Init(self._tab.Bytes, x) + return obj + return None + + # WebStreamMsg + def IsWebsocketStream(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(24)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # WebStreamMsg + def StatusCode(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(26)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 0 + + # WebStreamMsg + def WebsocketDataType(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(28)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 126 + + # WebStreamMsg + def MsgPriority(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(30)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 10 + + # WebStreamMsg + def CloseDueToRequestConnectionFailure(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(32)) + if o != 0: + return bool(self._tab.Get(octoflatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # WebStreamMsg + def BodyReadTimeHighWaterMarkMs(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(34)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 0 + + # WebStreamMsg + def SocketSendTimeHighWaterMarkMs(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(36)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 0 + + # WebStreamMsg + def MultipartReadsPerSecond(self): + o = octoflatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(38)) + if o != 0: + return self._tab.Get(octoflatbuffers.number_types.Uint8Flags, o + self._tab.Pos) + return 0 + +def WebStreamMsgStart(builder: octoflatbuffers.Builder): + builder.StartObject(18) + +def Start(builder: octoflatbuffers.Builder): + WebStreamMsgStart(builder) + +def WebStreamMsgAddStreamId(builder: octoflatbuffers.Builder, streamId: int): + builder.PrependUint32Slot(0, streamId, 0) + +def AddStreamId(builder: octoflatbuffers.Builder, streamId: int): + WebStreamMsgAddStreamId(builder, streamId) + +def WebStreamMsgAddIsOpenMsg(builder: octoflatbuffers.Builder, isOpenMsg: bool): + builder.PrependBoolSlot(1, isOpenMsg, 0) + +def AddIsOpenMsg(builder: octoflatbuffers.Builder, isOpenMsg: bool): + WebStreamMsgAddIsOpenMsg(builder, isOpenMsg) + +def WebStreamMsgAddIsCloseMsg(builder: octoflatbuffers.Builder, isCloseMsg: bool): + builder.PrependBoolSlot(2, isCloseMsg, 0) + +def AddIsCloseMsg(builder: octoflatbuffers.Builder, isCloseMsg: bool): + WebStreamMsgAddIsCloseMsg(builder, isCloseMsg) + +def WebStreamMsgAddIsDataTransmissionDone(builder: octoflatbuffers.Builder, isDataTransmissionDone: bool): + builder.PrependBoolSlot(3, isDataTransmissionDone, 0) + +def AddIsDataTransmissionDone(builder: octoflatbuffers.Builder, isDataTransmissionDone: bool): + WebStreamMsgAddIsDataTransmissionDone(builder, isDataTransmissionDone) + +def WebStreamMsgAddIsControlFlagsOnly(builder: octoflatbuffers.Builder, isControlFlagsOnly: bool): + builder.PrependBoolSlot(4, isControlFlagsOnly, 1) + +def AddIsControlFlagsOnly(builder: octoflatbuffers.Builder, isControlFlagsOnly: bool): + WebStreamMsgAddIsControlFlagsOnly(builder, isControlFlagsOnly) + +def WebStreamMsgAddFullStreamDataSize(builder: octoflatbuffers.Builder, fullStreamDataSize: int): + builder.PrependInt64Slot(5, fullStreamDataSize, -1) + +def AddFullStreamDataSize(builder: octoflatbuffers.Builder, fullStreamDataSize: int): + WebStreamMsgAddFullStreamDataSize(builder, fullStreamDataSize) + +def WebStreamMsgAddData(builder: octoflatbuffers.Builder, data: int): + builder.PrependUOffsetTRelativeSlot(6, octoflatbuffers.number_types.UOffsetTFlags.py_type(data), 0) + +def AddData(builder: octoflatbuffers.Builder, data: int): + WebStreamMsgAddData(builder, data) + +def WebStreamMsgStartDataVector(builder, numElems: int) -> int: + return builder.StartVector(1, numElems, 1) + +def StartDataVector(builder, numElems: int) -> int: + return WebStreamMsgStartDataVector(builder, numElems) + +def WebStreamMsgAddDataCompression(builder: octoflatbuffers.Builder, dataCompression: int): + builder.PrependInt8Slot(7, dataCompression, 0) + +def AddDataCompression(builder: octoflatbuffers.Builder, dataCompression: int): + WebStreamMsgAddDataCompression(builder, dataCompression) + +def WebStreamMsgAddOriginalDataSize(builder: octoflatbuffers.Builder, originalDataSize: int): + builder.PrependUint64Slot(8, originalDataSize, 0) + +def AddOriginalDataSize(builder: octoflatbuffers.Builder, originalDataSize: int): + WebStreamMsgAddOriginalDataSize(builder, originalDataSize) + +def WebStreamMsgAddHttpInitialContext(builder: octoflatbuffers.Builder, httpInitialContext: int): + builder.PrependUOffsetTRelativeSlot(9, octoflatbuffers.number_types.UOffsetTFlags.py_type(httpInitialContext), 0) + +def AddHttpInitialContext(builder: octoflatbuffers.Builder, httpInitialContext: int): + WebStreamMsgAddHttpInitialContext(builder, httpInitialContext) + +def WebStreamMsgAddIsWebsocketStream(builder: octoflatbuffers.Builder, isWebsocketStream: bool): + builder.PrependBoolSlot(10, isWebsocketStream, 0) + +def AddIsWebsocketStream(builder: octoflatbuffers.Builder, isWebsocketStream: bool): + WebStreamMsgAddIsWebsocketStream(builder, isWebsocketStream) + +def WebStreamMsgAddStatusCode(builder: octoflatbuffers.Builder, statusCode: int): + builder.PrependUint16Slot(11, statusCode, 0) + +def AddStatusCode(builder: octoflatbuffers.Builder, statusCode: int): + WebStreamMsgAddStatusCode(builder, statusCode) + +def WebStreamMsgAddWebsocketDataType(builder: octoflatbuffers.Builder, websocketDataType: int): + builder.PrependInt8Slot(12, websocketDataType, 126) + +def AddWebsocketDataType(builder: octoflatbuffers.Builder, websocketDataType: int): + WebStreamMsgAddWebsocketDataType(builder, websocketDataType) + +def WebStreamMsgAddMsgPriority(builder: octoflatbuffers.Builder, msgPriority: int): + builder.PrependInt8Slot(13, msgPriority, 10) + +def AddMsgPriority(builder: octoflatbuffers.Builder, msgPriority: int): + WebStreamMsgAddMsgPriority(builder, msgPriority) + +def WebStreamMsgAddCloseDueToRequestConnectionFailure(builder: octoflatbuffers.Builder, closeDueToRequestConnectionFailure: bool): + builder.PrependBoolSlot(14, closeDueToRequestConnectionFailure, 0) + +def AddCloseDueToRequestConnectionFailure(builder: octoflatbuffers.Builder, closeDueToRequestConnectionFailure: bool): + WebStreamMsgAddCloseDueToRequestConnectionFailure(builder, closeDueToRequestConnectionFailure) + +def WebStreamMsgAddBodyReadTimeHighWaterMarkMs(builder: octoflatbuffers.Builder, bodyReadTimeHighWaterMarkMs: int): + builder.PrependUint16Slot(15, bodyReadTimeHighWaterMarkMs, 0) + +def AddBodyReadTimeHighWaterMarkMs(builder: octoflatbuffers.Builder, bodyReadTimeHighWaterMarkMs: int): + WebStreamMsgAddBodyReadTimeHighWaterMarkMs(builder, bodyReadTimeHighWaterMarkMs) + +def WebStreamMsgAddSocketSendTimeHighWaterMarkMs(builder: octoflatbuffers.Builder, socketSendTimeHighWaterMarkMs: int): + builder.PrependUint16Slot(16, socketSendTimeHighWaterMarkMs, 0) + +def AddSocketSendTimeHighWaterMarkMs(builder: octoflatbuffers.Builder, socketSendTimeHighWaterMarkMs: int): + WebStreamMsgAddSocketSendTimeHighWaterMarkMs(builder, socketSendTimeHighWaterMarkMs) + +def WebStreamMsgAddMultipartReadsPerSecond(builder: octoflatbuffers.Builder, multipartReadsPerSecond: int): + builder.PrependUint8Slot(17, multipartReadsPerSecond, 0) + +def AddMultipartReadsPerSecond(builder: octoflatbuffers.Builder, multipartReadsPerSecond: int): + WebStreamMsgAddMultipartReadsPerSecond(builder, multipartReadsPerSecond) + +def WebStreamMsgEnd(builder: octoflatbuffers.Builder) -> int: + return builder.EndObject() + +def End(builder: octoflatbuffers.Builder) -> int: + return WebStreamMsgEnd(builder) diff --git a/octoapp/compat.py b/octoapp/compat.py index f0f93bb..043aaf6 100644 --- a/octoapp/compat.py +++ b/octoapp/compat.py @@ -2,7 +2,7 @@ from .interfaces import IApiRouteHandler, ISmartPauseHandler, IRelayWebSocketProvider, ILocalAuth, IRelayWebcamStreamDetector, ISlipstreamHandler, IWebRequestHandler, ICommandWebsocketProviderBuilder -# Some of the features we need to integrate into the octoapp package only exist on +# Some of the features we need to integrate into the octoeverywhere package only exist on # some platforms. This is basically an interface that allows us to dynamically control # if some objects are available depending on the platform. class Compat: diff --git a/octoapp/debugprofiler.py b/octoapp/debugprofiler.py index e9e26f1..cca1eac 100644 --- a/octoapp/debugprofiler.py +++ b/octoapp/debugprofiler.py @@ -1,6 +1,7 @@ import time -import logging from enum import Enum +from typing import Any +from .logging import LoggerLike # A list of possible features that can be profiled. @@ -80,7 +81,7 @@ class DebugProfiler: } - def __init__(self, logger:logging.Logger, feature:DebugProfilerFeatures, disableAutoStart=False) -> None: + def __init__(self, logger: LoggerLike, feature:DebugProfilerFeatures, disableAutoStart=False) -> None: self.Logger = logger self.Feature = feature self.Profiler = None @@ -95,9 +96,9 @@ def __enter__(self): self.StartProfile() return self - # Support using for easy integration. - def __exit__(self, exc_type, exc_value, traceback): + # Support using for easy integration. + def __exit__(self, exc_type:Any, exc_value:Any, traceback:Any) -> None: self.StopProfile() @@ -178,7 +179,7 @@ class MemoryProfiler(): _EnableProfiling = False - def __init__(self, logger:logging.Logger) -> None: + def __init__(self, logger: LoggerLike) -> None: self.Logger = logger self.Tracker = None self._TakeMemoryProfileSnapshot() diff --git a/octoapp/deviceid.py b/octoapp/deviceid.py index 389e6f6..9928894 100644 --- a/octoapp/deviceid.py +++ b/octoapp/deviceid.py @@ -3,8 +3,7 @@ import platform import subprocess from typing import Optional - -from octoapp.logging import LoggerLike +from .logging import LoggerLike from .sentry import Sentry @@ -28,7 +27,7 @@ def __init__(self, logger: LoggerLike) -> None: self.Logger = logger - # Get's a unique ID for the platform. The ID should be unique per platform and ideally not change even when the OS is re-installed. + # Gets a unique ID for the platform. The ID should be unique per platform and ideally not change even when the OS is re-installed. # This ID can't not be written to disk it must come from the system level some how. # If nothing can be found, None is return. def GetId(self) -> Optional[str]: diff --git a/octoapp/dnstest.py b/octoapp/dnstest.py index 0d1ea9b..fff222b 100644 --- a/octoapp/dnstest.py +++ b/octoapp/dnstest.py @@ -1,12 +1,13 @@ import time -import logging import dns.resolver +from .logging import LoggerLike + # Created to the DNS resolution of our URLS when the websocket claims it can't connect due to DNS issues. class DnsTest: - def __init__(self, logger: logging.Logger) -> None: + def __init__(self, logger: LoggerLike) -> None: self.Logger = logger diff --git a/octoapp/hostcommon.py b/octoapp/hostcommon.py index 6149b39..9523719 100644 --- a/octoapp/hostcommon.py +++ b/octoapp/hostcommon.py @@ -1,6 +1,6 @@ import os -import random import string +import secrets from typing import Optional # Common functions that the hosts might need to use. @@ -12,15 +12,28 @@ class HostCommon: c_OctoAppPrinterIdMaxLength = 60 c_OctoAppPrinterIdMinLength = 40 + # These are the bounds for the private keys. Originally they were 128chars, but after a change we moved them + # down to 80, which is still way more than enough. But some older installs still use the 128 length, so we have to allow it. + c_OctoAppPrivateKeyMinLength = 80 + c_OctoAppPrivateKeyMaxLength = 128 + # Returns a new printer Id. This needs to be crypo-random to make sure it's not predictable. @staticmethod - def GeneratePrinterId(): - return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(HostCommon.c_OctoAppPrinterIdMaxLength)) + def GeneratePrinterId() -> str: + return ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(HostCommon.c_OctoAppPrinterIdMaxLength)) + + # Returns a new private key. This needs to be crypo-random to make sure it's not predictable. + @staticmethod + def GeneratePrivateKey() -> str: + return ''.join(secrets.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(HostCommon.c_OctoAppPrivateKeyMinLength)) @staticmethod def IsPrinterIdValid(printerId:Optional[str]) -> bool: return printerId is not None and len(printerId) >= HostCommon.c_OctoAppPrinterIdMinLength and len(printerId) <= HostCommon.c_OctoAppPrinterIdMaxLength + @staticmethod + def IsPrivateKeyValid(privateKey:Optional[str]) -> bool: + return privateKey is not None and len(privateKey) >= HostCommon.c_OctoAppPrivateKeyMinLength and len(privateKey) <= HostCommon.c_OctoAppPrivateKeyMaxLength # This will restart the plugin or if running in OctoPrint restart OctoPrint! # Only use if absolutely needed! diff --git a/octoapp/httpresult.py b/octoapp/httpresult.py index 44c37bb..88932be 100644 --- a/octoapp/httpresult.py +++ b/octoapp/httpresult.py @@ -5,7 +5,7 @@ from requests.structures import CaseInsensitiveDict from octoapp.logging import LoggerLike -from .buffer import Buffer, ByteLike +from .buffer import Buffer from .Proto.DataCompression import DataCompression # Easy to use types. @@ -33,7 +33,7 @@ def __init__(self, didFallback:bool, fullBodyBuffer:Optional[Buffer]=None, requestLibResponseObj:Optional[requests.Response]=None, - customBodyStreamCallback:Optional[Callable[[], Buffer]]=None, + customBodyStreamCallback:Optional[Callable[[], Optional[Buffer]]]=None, customBodyStreamClosedCallback:Optional[Callable[[],None]]=None ): # Status code isn't a property because some things need to set it externally to the class. (Result.StatusCode = 302) @@ -121,7 +121,8 @@ def BodyBufferPreCompressSize(self) -> int: @property - def GetCustomBodyStreamCallback(self) -> Optional[Callable[[], Buffer]]: + def GetCustomBodyStreamCallback(self) -> Optional[Callable[[], Optional[Buffer]]]: + # This callback can return None, which indicates the stream is done or there was an error. return self._customBodyStreamCallback @@ -157,7 +158,8 @@ def ReadAllContentFromStreamResponse(self, logger:LoggerLike) -> None: # Ensure we have a stream to read. if self._requestLibResponseObj is None: raise Exception("ReadAllContentFromStreamResponse was called on a result with no request lib Response object.") - buffer:Optional[ByteLike] = None + # It's more efficient to gather the data in a single buffer, and append together at the end. + buffers:list[bytes | bytearray] = [] # In the past, we used iter_content, but it has a lot of overhead and also doesn't read all available data, it will only read a chunk if the transfer encoding is chunked. # This isn't great because it's slow and also we don't need to reach each chunk, process it, just to dump it in a buffer and read another. @@ -182,28 +184,18 @@ def ReadAllContentFromStreamResponse(self, logger:LoggerLike) -> None: # This is weird, but there can be lingering data in response.content, so add that if there is any. # See doBodyRead for more details. if len(self._requestLibResponseObj.content) > 0: - if buffer is None: - buffer = self._requestLibResponseObj.content - else: - buffer += self._requestLibResponseObj.content + buffers.append(self._requestLibResponseObj.content) # Break out when we are done. break # If we aren't done, append the buffer. - if buffer is None: - buffer = data - else: - buffer += data + buffers.append(data) except Exception as e: - lengthStr = "[buffer is None]" if buffer is None else str(len(buffer)) + bufferLength = sum(len(p) for p in buffers) + lengthStr = "[buffer is None]" if bufferLength == 0 else str(bufferLength) logger.warning(f"ReadAllContentFromStreamResponse got an exception. We will return the current buffer length of {lengthStr}, exception: {e}") - # Ensure we got something, as after this callers will expect an object to be there. - if buffer is None: - # If the buffer is None, we need to set it to a bytearray, since that's what we expect. - # This will be a empty buffer. - buffer = bytearray() - self.SetFullBodyBuffer(Buffer(buffer)) + self.SetFullBodyBuffer(Buffer(b''.join(buffers))) # We need to support the with keyword incase we have an actual Response object. diff --git a/octoapp/httpsessions.py b/octoapp/httpsessions.py index 9449937..1bdc926 100644 --- a/octoapp/httpsessions.py +++ b/octoapp/httpsessions.py @@ -4,7 +4,7 @@ import requests from requests import Session -from octoapp.logging import LoggerLike +from .logging import LoggerLike # A common class to cache http sessions per host. # This makes the connections more efficient as we can reuse the connections and the session isn't created every time. @@ -13,7 +13,7 @@ class HttpSessions: _Instance:"HttpSessions" = None #pyright: ignore[reportAssignmentType] @staticmethod - def Init(logger:LoggerLike): + def Init(logger: LoggerLike): HttpSessions._Instance = HttpSessions(logger) @@ -22,7 +22,7 @@ def Get(): return HttpSessions._Instance - def __init__(self, logger:LoggerLike): + def __init__(self, logger: LoggerLike): self.Logger = logger self.Sessions:Dict[str, Session] = {} self.SessionsLock = threading.Lock() diff --git a/octoapp/mdns.py b/octoapp/mdns.py index 4ee1988..1857dae 100644 --- a/octoapp/mdns.py +++ b/octoapp/mdns.py @@ -5,7 +5,8 @@ from typing import Any, Dict, List, Optional import dns.resolver -from octoapp.logging import LoggerLike + +from .logging import LoggerLike from .localip import LocalIpHelper # A helper class to resolve mdns domain names to IP addresses, since the request lib doesn't support @@ -28,7 +29,7 @@ class MDns: @staticmethod - def Init(logger:LoggerLike, pluginDataFolderPath:str) -> None: + def Init(logger: LoggerLike, pluginDataFolderPath:str) -> None: MDns._Instance = MDns(logger, pluginDataFolderPath) @@ -37,7 +38,7 @@ def Get() -> "MDns": return MDns._Instance - def __init__(self, logger:LoggerLike, pluginDataFolderPath:str) -> None: + def __init__(self, logger: LoggerLike, pluginDataFolderPath:str) -> None: self.Logger = logger # Init our DNS name cache. @@ -99,7 +100,7 @@ def TryToResolveIfLocalHostnameFound(self, url:str) -> Optional[str]: # If we don't get something back, we failed to resolve. if resolveResult is None: - self.LogDebug("mDNS found a .local domain to resolve, but it failed to resolve. hostname: "+str(hostname) + ", url: "+str(url)) + self.Logger.info("mDNS found a .local domain to resolve, but it failed to resolve. hostname: "+str(hostname) + ", url: "+str(url)) return None # Inject the IP resolved into the url. @@ -163,7 +164,7 @@ def _TryToResolve(self, domain:str) -> Optional[str]: # Only allow 3 attempts to successfully resolve. attempt += 1 if attempt > 3: - self.LogDebug("Failed to resolve mdns for domain "+str(domain)) + self.Logger.info("Failed to resolve mdns for domain "+str(domain)) # Return none to indicate a failure. return None @@ -297,7 +298,7 @@ def GetSameLanIp(self, ipList:List[str]) -> str: c = 0 for ip in ipList: if matches[c] is True: - self.LogDebug("MDNS got to end of of the IP string with multiple matches, so we will just return this: "+str(ip)) + self.Logger.info("MDNS got to end of of the IP string with multiple matches, so we will just return this: "+str(ip)) return ip c += 1 diff --git a/octoapp/notificationshandler.py b/octoapp/notificationshandler.py index 0181301..edf212c 100644 --- a/octoapp/notificationshandler.py +++ b/octoapp/notificationshandler.py @@ -1,8 +1,9 @@ import math import threading import time +import secrets +import string from typing import Any, Dict, List, Optional, Tuple -from uuid import uuid4 from .bedcooldownwatcher import BedCooldownWatcher from .buffer import ByteLikeOrMemoryView @@ -129,7 +130,7 @@ def _RecoverOrRestForNewPrint(self, printCookie:Optional[str]): # Each time a print starts, we generate a fixed length random id to identify it. # This id is used to globally identify the print for the user, so it needs to have high entropy. - printId = uuid4().hex + printId = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(32)) # Always make a new print info for this new print. # This is where we will store all of the vars for this print, and it's also written to disk if we need to recover the info. @@ -693,7 +694,7 @@ def _getCurrentProgressFloat(self) -> float: Sentry.OnExceptionNoSend("_getCurrentProgressFloat failed to compute progress.", e) # On failure, default to what OctoPrint has reported. - return float(self.FallbackProgressInt) if isinstance(self.FallbackProgressInt, int) else 0.0 + return float(self.FallbackProgressInt) # Sends the event diff --git a/octoapp/octohttprequest.py b/octoapp/octohttprequest.py index 66cb161..94a972a 100644 --- a/octoapp/octohttprequest.py +++ b/octoapp/octohttprequest.py @@ -1,7 +1,6 @@ import platform from typing import Dict, Optional - -from octoapp.logging import LoggerLike +from .logging import LoggerLike from .mdns import MDns from .buffer import BufferOrNone @@ -10,6 +9,7 @@ from .httpresult import HttpResult from .httpsessions import HttpSessions from .octostreammsgbuilder import OctoStreamMsgBuilder + from .Proto.PathTypes import PathTypes from .Proto.HttpInitialContext import HttpInitialContext @@ -20,6 +20,7 @@ class OctoHttpRequest: LocalOctoPrintPort = 5000 LocalHostAddress = "127.0.0.1" DisableHttpRelay = False + LocalHostUseHttps = False @staticmethod def SetLocalHttpProxyPort(port:int) -> None: @@ -49,6 +50,13 @@ def SetLocalHostAddress(address:str) -> None: def GetLocalhostAddress() -> str: return OctoHttpRequest.LocalHostAddress + @staticmethod + def SetLocalHostUseHttps(address:bool): + OctoHttpRequest.LocalHostUseHttps = address + @staticmethod + def GetLocalHostUseHttps() -> bool: + return OctoHttpRequest.LocalHostUseHttps + @staticmethod def SetDisableHttpRelay(disableHttpRelay:bool) -> None: OctoHttpRequest.DisableHttpRelay = disableHttpRelay @@ -73,7 +81,7 @@ def GetPathType(url:str) -> int: # The main point of this function is to abstract away the logic around relative paths, absolute URLs, and the fallback logic # we use for different ports. See the comments in the function for details. @staticmethod - def MakeHttpCallOctoStreamHelper(logger:LoggerLike, httpInitialContext:HttpInitialContext, method:str, headers:Dict[str, str], data:BufferOrNone=None) -> Optional[HttpResult]: + def MakeHttpCallOctoStreamHelper(logger: LoggerLike, httpInitialContext:HttpInitialContext, method:str, headers:Dict[str, str], data:BufferOrNone=None) -> Optional[HttpResult]: # Get the vars we need from the octostream initial context. path = OctoStreamMsgBuilder.BytesToString(httpInitialContext.Path()) if path is None: @@ -89,13 +97,13 @@ def MakeHttpCallOctoStreamHelper(logger:LoggerLike, httpInitialContext:HttpIniti # The X-Forwarded-Host header will tell the OctoPrint server the correct place to set the location redirect header. # However, for calls that aren't proxy calls, things like local snapshot requests and such, we want to allow redirects to be more robust. @staticmethod - def MakeHttpCall(logger:LoggerLike, pathOrUrl:str, pathOrUrlType:int, method:str, headers:Optional[Dict[str, str]]=None, data:BufferOrNone=None, allowRedirects=False) -> Optional[HttpResult]: + def MakeHttpCall(logger: LoggerLike, pathOrUrl:str, pathOrUrlType:int, method:str, headers:Optional[Dict[str, str]]=None, data:BufferOrNone=None, allowRedirects=False) -> Optional[HttpResult]: # First of all, we need to figure out what the URL is. There are two options # # 1) Absolute URLs # These are the easiest, because we just want to make a request to exactly what the absolute URL is. These are used # when the OctoPrint portal is trying to make an local LAN http request to the same device or even a different device. - # For these to work properly on a remote browser, the OctoApp service will detect and convert the URLs in to encoded relative + # For these to work properly on a remote browser, the OctoEverywhere service will detect and convert the URLs in to encoded relative # URLs for the portal. This ensures when the remote browser tries to access the HTTP endpoint, it will hit OctoApp. The OctoApp # server detects the special relative URL, decodes the absolute URL, and sends that in the OctoMessage as "AbsUrl". For these URLs we just try # to hit them and we take whatever we get, we don't care if fails or not. @@ -118,16 +126,19 @@ def MakeHttpCall(logger:LoggerLike, pathOrUrl:str, pathOrUrlType:int, method:str # get a 404 back try the haproxy. This adds a little bit of unneeded overhead, but it works really well to cover all of the cases. # Setup the protocol we need to use for the http proxy. We need to use the same protocol that was detected. + localServiceProtocol = "http://" + if OctoHttpRequest.LocalHostUseHttps: + localServiceProtocol = "https://" httpProxyProtocol = "http://" if OctoHttpRequest.LocalHttpProxyIsHttps: httpProxyProtocol = "https://" # Figure out the main and fallback url. url = "" - fallbackUrl = None - fallbackWebcamUrl = None - fallbackLocalIpOctoPrintPortSuffix = None - fallbackLocalIpHttpProxySuffix = None + fallbackUrl:Optional[str] = None + fallbackWebcamUrl:Optional[str] = None + fallbackLocalIpDirectServicePortSuffix:Optional[str] = None + fallbackLocalIpHttpProxySuffix:Optional[str] = None if pathOrUrlType == PathTypes.Relative: # Note! @@ -142,7 +153,7 @@ def MakeHttpCall(logger:LoggerLike, pathOrUrl:str, pathOrUrlType:int, method:str # The main URL is directly to this OctoPrint instance # This URL will only every be http, it can't be https. - url = "http://" + OctoHttpRequest.LocalHostAddress + ":" + str(OctoHttpRequest.LocalOctoPrintPort) + pathOrUrl + url = localServiceProtocol + OctoHttpRequest.LocalHostAddress + ":" + str(OctoHttpRequest.LocalOctoPrintPort) + pathOrUrl # The fallback URL is to where we think the http proxy port is. # For this address, we need set the protocol correctly depending if the client detected https @@ -164,7 +175,7 @@ def MakeHttpCall(logger:LoggerLike, pathOrUrl:str, pathOrUrlType:int, method:str # Note we only build the suffix part of the string here, because we don't want to do the local IP detection if we don't have to. # Also note this will only work for OctoPrint pages. # This case only seems to apply to OctoPrint instances running on Windows. - fallbackLocalIpOctoPrintPortSuffix = ":" + str(OctoHttpRequest.LocalOctoPrintPort) + pathOrUrl + fallbackLocalIpDirectServicePortSuffix = ":" + str(OctoHttpRequest.LocalOctoPrintPort) + pathOrUrl fallbackLocalIpHttpProxySuffix = ":" + str(OctoHttpRequest.LocalHttpProxyPort) + pathOrUrl # If all else fails, and because this logic isn't perfect, yet, we will also try to fallback to the assumed webcam port. @@ -248,19 +259,19 @@ def MakeHttpCall(logger:LoggerLike, pathOrUrl:str, pathOrUrlType:int, method:str # With the local IP, first try to use the http proxy URL, since it's the most likely to be bound to the public IP and not firewalled. # It's important we use the right http proxy protocol with the http proxy port. localIpFallbackUrl = httpProxyProtocol + localIp + fallbackLocalIpHttpProxySuffix - ret = OctoHttpRequest.MakeHttpCallAttempt(logger, "Local IP Http Proxy Fallback", method, localIpFallbackUrl, headers, data, mainResult, True, fallbackLocalIpOctoPrintPortSuffix, allowRedirects) + ret = OctoHttpRequest.MakeHttpCallAttempt(logger, "Local IP Http Proxy Fallback", method, localIpFallbackUrl, headers, data, mainResult, True, fallbackLocalIpDirectServicePortSuffix, allowRedirects) # If the function reports the chain is done, the next fallback URL is invalid and we should always return # whatever is in the Response, even if it's None. if ret.IsChainDone: return ret.Result # We should have a fallbackLocalIpHttpProxySuffix if we are here. - if fallbackLocalIpOctoPrintPortSuffix is None: + if fallbackLocalIpDirectServicePortSuffix is None: logger.error("Main request failed and no fallbackLocalIpOctoPrintPortSuffix was provided. This is a critical error and should be reported to the OctoApp team.") return ret.Result # Now try the OcotoPrint direct port with the local IP. - localIpFallbackUrl = "http://" + localIp + fallbackLocalIpOctoPrintPortSuffix + localIpFallbackUrl = "http://" + localIp + fallbackLocalIpDirectServicePortSuffix ret = OctoHttpRequest.MakeHttpCallAttempt(logger, "Local IP fallback", method, localIpFallbackUrl, headers, data, mainResult, True, fallbackWebcamUrl, allowRedirects) # If the function reports the chain is done, the next fallback URL is invalid and we should always return # whatever is in the Response, even if it's None. @@ -300,7 +311,7 @@ def Result(self) -> Optional[HttpResult]: # This function should always return a AttemptResult object. @staticmethod - def MakeHttpCallAttempt(logger:LoggerLike, attemptName:str, method:str, url:str, headers:Optional[Dict[str,str]], data:BufferOrNone, mainResult:Optional[HttpResult], isFallback:bool, nextFallbackUrl:Optional[str], allowRedirects:bool=False) -> AttemptResult: + def MakeHttpCallAttempt(logger: LoggerLike, attemptName:str, method:str, url:str, headers:Optional[Dict[str,str]], data:BufferOrNone, mainResult:Optional[HttpResult], isFallback:bool, nextFallbackUrl:Optional[str], allowRedirects:bool=False) -> AttemptResult: # The requests lib can accept any "byte like" object. We use this to force the type to be bytes, so pyright is happy. dataBuffer:Optional[bytes] = None if data is None else data.GetBytesLike() #pyright: ignore[reportAssignmentType] diff --git a/octoapp/octostreammsgbuilder.py b/octoapp/octostreammsgbuilder.py index 41ba45d..c705459 100644 --- a/octoapp/octostreammsgbuilder.py +++ b/octoapp/octostreammsgbuilder.py @@ -21,10 +21,11 @@ def BuildHandshakeSyn( rasKeyVersionInt:int, summonMethod:int, serverHostType:int, - isCompanion:bool, osType:int, receiveCompressionType:int, - deviceId:Optional[str] + deviceId:Optional[str], + isCompanion:bool, + isDockerContainer:bool ) -> Tuple[Buffer, int, int]: # Get a buffer builder = OctoStreamMsgBuilder.CreateBuffer(500) @@ -52,6 +53,7 @@ def BuildHandshakeSyn( HandshakeSyn.AddSummonMethod(builder, summonMethod) HandshakeSyn.AddServerHost(builder, serverHostType) HandshakeSyn.AddIsCompanion(builder, isCompanion) + HandshakeSyn.AddIsDockerContainer(builder, isDockerContainer) if localIpOffset is not None: HandshakeSyn.AddLocalDeviceIp(builder, localIpOffset) HandshakeSyn.AddLocalHttpProxyPort(builder, localHttpProxyPort) diff --git a/octoapp/printinfo.py b/octoapp/printinfo.py index 85cee4e..a8d5cfd 100644 --- a/octoapp/printinfo.py +++ b/octoapp/printinfo.py @@ -3,7 +3,6 @@ import time from pathlib import Path from typing import Any, Dict, Optional - from .logging import LoggerLike @@ -27,7 +26,7 @@ class PrintInfo: # Given a file path, this loads a print info if possible. # Returns None on failure. @staticmethod - def LoadFromFile(logger:LoggerLike, filePath:str) -> Optional["PrintInfo"]: + def LoadFromFile(logger: LoggerLike, filePath:str) -> Optional["PrintInfo"]: try: with open(filePath, "r", encoding="utf-8") as f: data = json.load(f) @@ -43,7 +42,7 @@ def LoadFromFile(logger:LoggerLike, filePath:str) -> Optional["PrintInfo"]: # Given a file path and required args, creates a new print context. # This will always return a PrintInfo! Even if it fails to write to disk. @staticmethod - def CreateNew(logger:LoggerLike, filePath:str, printCookie:str, printId:str) -> "PrintInfo": + def CreateNew(logger: LoggerLike, filePath:str, printCookie:str, printId:str) -> "PrintInfo": data = { PrintInfo.c_PrintCookieKey : printCookie, PrintInfo.c_PrintIdKey : printId, @@ -55,7 +54,7 @@ def CreateNew(logger:LoggerLike, filePath:str, printCookie:str, printId:str) -> return pi - def __init__(self, logger:LoggerLike, filePath:str, data:Dict[str,Any]) -> None: + def __init__(self, logger: LoggerLike, filePath:str, data:Dict[str,Any]) -> None: self.Logger = logger self.FilePath = filePath self.Data = data @@ -158,7 +157,7 @@ class PrintInfoManager: _Instance:"PrintInfoManager" = None #pyright: ignore[reportAssignmentType] @staticmethod - def Init(logger:LoggerLike, localStorageFolderPath:str): + def Init(logger: LoggerLike, localStorageFolderPath:str): PrintInfoManager._Instance = PrintInfoManager(logger, localStorageFolderPath) @@ -167,7 +166,7 @@ def Get(): return PrintInfoManager._Instance - def __init__(self, logger:LoggerLike, localStorageFolderPath:str) -> None: + def __init__(self, logger: LoggerLike, localStorageFolderPath:str) -> None: self.Logger = logger self.ContextFolderPath = os.path.join(localStorageFolderPath, PrintInfoManager.c_ContextsFolder) Path(self.ContextFolderPath).mkdir(parents=True, exist_ok=True) @@ -182,6 +181,7 @@ def GetPrintInfo(self, printCookie:Optional[str]) -> Optional[PrintInfo]: try: # If there's no cookie, return None. if printCookie is None: + self.Logger.debug("GetPrintInfo called with no cookie.") return None # First, see if the current context matches. @@ -200,6 +200,7 @@ def GetPrintInfo(self, printCookie:Optional[str]) -> Optional[PrintInfo]: if name == printCookieFileName: context = PrintInfo.LoadFromFile(self.Logger, fullPath) if context is None: + self.Logger.debug(f"Failed to load print context from {fullPath}.") self._DeleteFile(fullPath) else: self._DeleteFile(fullPath) @@ -216,8 +217,6 @@ def GetPrintInfo(self, printCookie:Optional[str]) -> Optional[PrintInfo]: # Clears all print infos. Note this should only be used when we absolutely know this is a new print start, # like on a new print start or something. def ClearAllPrintInfos(self) -> None: - self.Logger.info("Removing all print infos") - self.CurrentContext = None try: dirAndFiles = os.listdir(self.ContextFolderPath) for name in dirAndFiles: diff --git a/octoapp/repeattimer.py b/octoapp/repeattimer.py index 426316d..d196cb0 100644 --- a/octoapp/repeattimer.py +++ b/octoapp/repeattimer.py @@ -1,12 +1,12 @@ import threading from typing import Any, Callable - from .logging import LoggerLike + from .sentry import Sentry class RepeatTimer(threading.Thread): - def __init__(self, logger:LoggerLike, name:str, intervalSec:float, func:Callable[[], None]): + def __init__(self, logger: LoggerLike, name:str, intervalSec:float, func:Callable[[], None]): threading.Thread.__init__(self, name=name) self.stopEvent = threading.Event() self.logger = logger @@ -26,7 +26,7 @@ def run(self): self.callback() except Exception as e: Sentry.OnException("Exception in RepeatTimer thread.", e) - self.logger.info("RepeatTimer thread exit") + self.logger.debug("RepeatTimer thread exit") # Used to update the repeat interval. This can be called while the timer is running diff --git a/octoapp/threaddebug.py b/octoapp/threaddebug.py index 9abed66..dd9ffe1 100644 --- a/octoapp/threaddebug.py +++ b/octoapp/threaddebug.py @@ -1,13 +1,13 @@ import threading -import logging import time import sys import traceback +from .logging import LoggerLike class ThreadDebug: - def Start(self, logger:logging.Logger, delaySec:float): + def Start(self, logger: LoggerLike, delaySec:float): try: th = threading.Thread(target=self.threadWorker, args=(logger, delaySec)) th.start() @@ -15,7 +15,7 @@ def Start(self, logger:logging.Logger, delaySec:float): logger.error("Failed to start Thread Debug Thread: "+str(e)) - def threadWorker(self, logger:logging.Logger, delaySec:float): + def threadWorker(self, logger: LoggerLike, delaySec:float): while True: try: logger.info("ThreadDump - Starting Thread Dump") @@ -26,7 +26,7 @@ def threadWorker(self, logger:logging.Logger, delaySec:float): @staticmethod - def DoThreadDumpLogout(logger:logging.Logger): + def DoThreadDumpLogout(logger: LoggerLike): try: logger.info("ThreadDump - Starting Thread Dump") # pylint: disable=protected-access diff --git a/octoapp/util/__init__.py b/octoapp/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/octoapp/util/delayedcallback.py b/octoapp/util/delayedcallback.py new file mode 100644 index 0000000..a213c9c --- /dev/null +++ b/octoapp/util/delayedcallback.py @@ -0,0 +1,60 @@ +import threading +import logging +from typing import Any, Callable + +from octoapp.sentry import Sentry + + +# A simple class that fires a callback after a delay unless it's canceled. +class DelayedCallback(threading.Thread): + + @staticmethod + def Create(logger:logging.Logger, name:str, delaySec:float, func:Callable[[], None]) -> "DelayedCallback": + cb = DelayedCallback(logger, name, delaySec, func) + cb.start() + return cb + + + def __init__(self, logger:logging.Logger, name:str, delaySec:float, func:Callable[[], None]): + threading.Thread.__init__(self, name=name) + self.stopEvent = threading.Event() + self.logger = logger + self.delaySec = delaySec + self.callback = func + self.running = True + + + # Overwrite the thread function. + def run(self): + try: + # Wait for the delay to elapse, unless canceled. + self.logger.debug("DelayedCallback starting: "+self.name) + if self.stopEvent.wait(self.delaySec) is False: + # Ensure we don't fire the callback if we weren't asked to. + if self.is_alive() is False or self.running is False: + return + try: + self.callback() + except Exception as e: + Sentry.OnException("Exception in DelayedCallback thread.", e) + finally: + self.logger.debug("DelayedCallback thread exit: "+self.name) + + + # Returns if the timer is currently running or not. + def IsRunning(self) -> bool: + return self.running + + + # Used to cancel the timer before it fires. + def Cancel(self): + self.running = False + self.stopEvent.set() + + + def __enter__(self): + return self + + + def __exit__(self, exc_type:Any, exc_value:Any, traceback:Any): + self.Cancel() diff --git a/octoapp/websocketimpl.py b/octoapp/websocketimpl.py index b443cef..f0a8520 100644 --- a/octoapp/websocketimpl.py +++ b/octoapp/websocketimpl.py @@ -1,5 +1,7 @@ import queue +import ssl import threading +import logging from typing import Any, Dict, List, Callable, Optional import certifi @@ -14,6 +16,18 @@ # This class gives a bit of an abstraction over the normal ws class Client(IWebSocketClient): + # Allows us to still enable the websocket debug logs if we want. + @staticmethod + def SetWebsocketDebuggingLevel(debug:bool) -> None: + # The websocket lib logs quite a lot of stuff, even to info. It will also always logs errors, + # even after our handler had handled them. So we will disable it by default. + wsLibLogger = logging.getLogger("websocket") + if debug is False: + wsLibLogger.disabled = True + return + wsLibLogger.disabled = False + wsLibLogger.setLevel(logging.DEBUG) + def __init__( self, url:str, @@ -44,9 +58,7 @@ def __init__( # This is because the downstream work of the WS can be made faster if it's done in parallel self.SendQueue:queue.Queue[SendQueueContext] = queue.Queue() self.SendThread:threading.Thread = None #pyright: ignore[reportAttributeAccessIssue] - - # Used to log more details about what's going on with the websocket. - # websocket.enableTrace(True) + self.disableCertCheck = False # Used to indicate if the client has started to close this WS. If so, we won't fire # any errors. @@ -96,6 +108,12 @@ def OnError(ws:WebSocket, exception:Exception): ) + # This has it's own function so the caller very explicitly has to call it, rather than it being an init overload. + # If set to true, this websocket connection will not validate the cert it's connecting to. This should only be done locally! + def SetDisableCertCheck(self, disable:bool): + self.disableCertCheck = disable + + # Runs the websocket blocking until it closes. def RunUntilClosed(self, pingIntervalSec:Optional[int]=None, pingTimeoutSec:Optional[int]=None, pingPayload:str="") -> None: # @@ -115,8 +133,9 @@ def RunUntilClosed(self, pingIntervalSec:Optional[int]=None, pingTimeoutSec:Opti if pingTimeoutSec is None or pingTimeoutSec <= 0 or pingTimeoutSec > 60: pingTimeoutSec = 20 # Ensure that the ping timeout is set, otherwise the websocket will hang forever if the connection is lost. + # This is also important to ensure that NAT routers and load balancers keep the connection alive. if pingIntervalSec is None or pingIntervalSec <= 0: - pingIntervalSec = 600 + pingIntervalSec = 30 try: # Start the send queue thread if it hasn't been started. @@ -137,7 +156,12 @@ def RunUntilClosed(self, pingIntervalSec:Optional[int]=None, pingTimeoutSec:Opti if self.isClosed: return - self.Ws.run_forever(skip_utf8_validation=True, ping_interval=pingIntervalSec, ping_timeout=pingTimeoutSec, sslopt={"ca_certs":certifi.where()},ping_payload=pingPayload) #pyright: ignore[reportUnknownMemberType] + # Only if the client explicated called the function to disable this will we turn off cert verification. + sslopt={"ca_certs":certifi.where()} + if self.disableCertCheck: + sslopt = {"cert_reqs": ssl.CERT_NONE, "check_hostname": False} + + self.Ws.run_forever(skip_utf8_validation=True, ping_interval=pingIntervalSec, ping_timeout=pingTimeoutSec, sslopt=sslopt, ping_payload=pingPayload) #pyright: ignore[reportUnknownMemberType] except Exception as e: # There's a compat issue where run_forever will try to access "isAlive" when the socket is closing # "isAlive" apparently doesn't exist in some PY versions of thread, so this throws. We will ignore that error, @@ -286,7 +310,7 @@ def _SendQueueThread(self): self.handleWsError(e) finally: # When the send queue closes, make sure the websocket is closed. - # This is a safety, incase for some reason the websocket was open and we were told to close. + # This is a safety, in case for some reason the websocket was open and we were told to close. self._Close()