Hey @dunhou,
No, that question is completely valid. Yes, a GeDialog
or MessageData
which subscribes to a timer event will cost system resources. And when a user would have installed 100's of plugins using them, it would bring Cinema 4D to a crawl. But for a singular plugin there are no performance concerns.
But you also do not have to subscribe to a timer indefinitely. So, you can enable the timer once you have sent a mail and disable it once you have received a response for example. Find below a simple example for a message hook which manages URL request and shuts itself off once all requests have been processed. Adding new requests to it at runtime would also turn it on again.
Cheers,
Ferdinand
Result:
...
Timer message '2023-02-23 12:34:33.155866'
Timer message '2023-02-23 12:34:33.403811'
Timer message '2023-02-23 12:34:33.650063'
Successfully connected to 'https://www.python.org/'.
Failed to connect to 'https://www.google.com/foo' with status error: 'HTTP Error 404: Not Found'.
Timer message '2023-02-23 12:34:33.900924'
Successfully connected to 'https://www.google.com/'.
Failed to connect to 'https://www.foobar.com/' with status error: '<urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1123)>'.
Timer message '2023-02-23 12:34:34.149718'
Timer message '2023-02-23 12:34:34.410978'
Timer message '2023-02-23 12:34:34.656376'
Timer message '2023-02-23 12:34:34.903763'
Timer message '2023-02-23 12:34:35.152023'
Timer message '2023-02-23 12:34:35.399493'
Timer message '2023-02-23 12:34:35.926809' # Here the hook has shut itself off.
Code:
"""Provides an example for a timed message hook which shuts itself off once all tasks are done.
This is simply done by returning a variable for MessageData.GetTimer, allowing the hook to turn its
timer subscription on and off based on the "tasks" it has.
"""
import c4d
import typing
import urllib.request
import datetime
import time
from http.client import HTTPResponse
class HttpRequestThread (c4d.threading.C4DThread):
"""Wraps an http(s) request in a thread.
"""
def __init__(self, url: str, timeout: int = 10) -> None:
"""Initializes the thread with an url and timeout value.
"""
if not isinstance(url, str):
raise TypeError(f"{url = }")
if not isinstance(timeout, int):
raise TypeError(f"{timeout = }")
self._url : str = url
self._timeout: int = timeout
self._result: typing.Union[HTTPResponse, Exception] = Exception("Unexpected null result.")
def Main(self) -> None:
"""Makes the http(s) request.
"""
# Postpone things a bit because otherwise all requests will have been finished before
# Cinema 4D has fully started and we would not see any timer messages before the threads
# already did finish.
time.sleep(5)
# Make the request and store its error or result.
try:
self._result = urllib.request.urlopen(self._url, timeout=self._timeout)
except Exception as e:
self._result = e
def GetResult(self) -> str:
"""Returns a string representation of the http(s) response for the request made by the thread.
"""
if self.IsRunning():
raise RuntimeError("This thread is still running.")
res, url = self._result, self._url
if isinstance(res, HTTPResponse) and res.status == 200:
return f"Successfully connected to '{url}'."
elif isinstance(res, HTTPResponse):
return f"Failed to connect to '{url}' with status code '{res.status}'."
else:
return f"Failed to connect to '{url}' with status error: '{res}'."
class HttpRequestHandlerData (c4d.plugins.MessageData):
"""Realizes a message hook that runs http(s) request and reacts to their outcome.
"""
PLUGIN_ID: int = 1060594
def __init__(self) -> None:
"""Initializes the hook.
Normally feeding such hook would be more complex and will include sending messages to convey
new tasks. Here we just feed it a fixed amount of tasks on instantiation.
"""
self._httpRequestThreads: list[HttpRequestThread] = [
# Should not fail before the sun does collapse into itself.
HttpRequestThread(url=r"https://www.google.com/", timeout=10),
HttpRequestThread(url=r"https://www.python.org/", timeout=10),
# Will fail.
HttpRequestThread(url=r"https://www.foobar.com/", timeout=10),
HttpRequestThread(url=r"https://www.google.com/foo", timeout=10),
]
for thread in self._httpRequestThreads:
thread.Start()
super().__init__()
@property
def PendingRequestCount(self) -> int:
"""Returns the number of pending request objects.
"""
return len(self._httpRequestThreads)
def ProcessRequests(self) -> None:
"""Exemplifies a callback function which is called by the timer.
Here we use it to remove url threads once they have finished. Which in turn will mean
that once there are no url threads anymore, the timer will be stopped. And once there
is a new url thread, it will begin again.
"""
result: list[HttpRequestThread] = []
for thread in self._httpRequestThreads:
if thread.IsRunning():
result.append(thread)
continue
# Do something with a finished thread, we are just printing the result to the console.
thread.End()
print (thread.GetResult())
# Set the new collection of still ongoing threads.
self._httpRequestThreads = result
def GetTimer(self) -> int:
"""Enables a timer message for the plugin with a tick frequency of ~250ms when there
are pending requests and disables it when there are none.
Note that the tick frequency will be approximately 250ms and not exactly that value. For
values below 100ms the error will increase steadily and when setting the tick frequency to
for example 10ms one might end up with a tick history such as this:
[27ms, 68ms, 11ms, 112ms, 36ms, ...]
"""
return 250 if self.PendingRequestCount > 0 else 0
def CoreMessage(self, mid: int, _: c4d.BaseContainer) -> bool:
"""Called by Cinema 4D to convey core events.
"""
if mid == c4d.MSG_TIMER:
self.ProcessRequests()
print (f"Timer message '{datetime.datetime.now()}'")
return True
def RegisterPlugins() -> bool:
"""
"""
if not c4d.plugins.RegisterMessagePlugin(
HttpRequestHandlerData.PLUGIN_ID, "HttpRequestHandlerData", 0, HttpRequestHandlerData()):
print (f"Failed to register: {HttpRequestHandlerData}")
return False
if __name__ == "__main__":
RegisterPlugins()