add a ratelimit decorator to {VbanCmd}.sendtext()

ip kwarg renamed to host.
This commit is contained in:
onyx-and-iris 2026-03-02 23:20:45 +00:00
parent cf66ae252c
commit 86d0aa91c3
4 changed files with 42 additions and 29 deletions

View File

@ -84,18 +84,20 @@ class FactoryBase(VbanCmd):
def __init__(self, kind_id: str, **kwargs): def __init__(self, kind_id: str, **kwargs):
defaultkwargs = { defaultkwargs = {
'ip': 'localhost', 'host': 'localhost',
'port': 6980, 'port': 6980,
'streamname': 'Command1', 'streamname': 'Command1',
'bps': 256000, 'bps': 256000,
'channel': 0, 'channel': 0,
'ratelimit': 0.01, 'script_ratelimit': 0.05, # 20 commands per second, to avoid overloading Voicemeeter
'timeout': 5, 'timeout': 5, # timeout on socket operations, in seconds
'disable_rt_listeners': False, 'disable_rt_listeners': False,
'sync': False, 'sync': False,
'pdirty': False, 'pdirty': False,
'ldirty': False, 'ldirty': False,
} }
if 'ip' in kwargs:
defaultkwargs['host'] = kwargs.pop('ip') # for backwards compatibility
if 'subs' in kwargs: if 'subs' in kwargs:
defaultkwargs |= kwargs.pop('subs') # for backwards compatibility defaultkwargs |= kwargs.pop('subs') # for backwards compatibility
kwargs = defaultkwargs | kwargs kwargs = defaultkwargs | kwargs

View File

@ -1,6 +1,23 @@
import time
from typing import Iterator from typing import Iterator
def ratelimit(func):
"""ratelimit decorator for {VbanCmd}.sendtext, to prevent flooding the network with script requests."""
def wrapper(*args, **kwargs):
self, *rem = args
if self.script_ratelimit > 0:
now = time.time()
elapsed = now - self._last_script_request_time
if elapsed < self.script_ratelimit:
time.sleep(self.script_ratelimit - elapsed)
self._last_script_request_time = time.time()
return func(*args, **kwargs)
return wrapper
def cache_bool(func, param): def cache_bool(func, param):
"""Check cache for a bool prop""" """Check cache for a bool prop"""

View File

@ -17,7 +17,7 @@ from .packet.headers import (
) )
from .packet.ping0 import VbanPing0Payload, VbanServerType from .packet.ping0 import VbanPing0Payload, VbanServerType
from .subject import Subject from .subject import Subject
from .util import bump_framecounter, deep_merge from .util import bump_framecounter, deep_merge, ratelimit
from .worker import Producer, Subscriber, Updater from .worker import Producer, Subscriber, Updater
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -38,7 +38,7 @@ class VbanCmd(abc.ABC):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__) self.logger = logger.getChild(self.__class__.__name__)
self.event = Event({k: kwargs.pop(k) for k in ('pdirty', 'ldirty')}) self.event = Event({k: kwargs.pop(k) for k in ('pdirty', 'ldirty')})
if not kwargs['ip']: if not kwargs['host']:
kwargs |= self._conn_from_toml() kwargs |= self._conn_from_toml()
for attr, val in kwargs.items(): for attr, val in kwargs.items():
setattr(self, attr, val) setattr(self, attr, val)
@ -52,9 +52,9 @@ class VbanCmd(abc.ABC):
self.cache = {} self.cache = {}
self._pdirty = False self._pdirty = False
self._ldirty = False self._ldirty = False
self._script = str()
self.stop_event = None self.stop_event = None
self.producer = None self.producer = None
self._last_script_request_time = 0
@abc.abstractmethod @abc.abstractmethod
def __str__(self): def __str__(self):
@ -113,7 +113,7 @@ class VbanCmd(abc.ABC):
self.producer.start() self.producer.start()
self.logger.info( self.logger.info(
"Successfully logged into VBANCMD {kind} with ip='{ip}', port={port}, streamname='{streamname}'".format( "Successfully logged into VBANCMD {kind} with host='{host}', port={port}, streamname='{streamname}'".format(
**self.__dict__ **self.__dict__
) )
) )
@ -149,8 +149,8 @@ class VbanCmd(abc.ABC):
self.sock.settimeout(0.5) self.sock.settimeout(0.5)
try: try:
self.sock.sendto(ping_packet, (socket.gethostbyname(self.ip), self.port)) self.sock.sendto(ping_packet, (socket.gethostbyname(self.host), self.port))
self.logger.debug(f'PING sent to {self.ip}:{self.port}') self.logger.debug(f'PING sent to {self.host}:{self.port}')
start_time = time.time() start_time = time.time()
response_count = 0 response_count = 0
@ -193,11 +193,13 @@ class VbanCmd(abc.ABC):
f'PING timeout after {timeout}s, received {response_count} non-PONG packets' f'PING timeout after {timeout}s, received {response_count} non-PONG packets'
) )
raise VBANCMDConnectionError( raise VBANCMDConnectionError(
f'PING timeout: No response from {self.ip}:{self.port} after {timeout}s' f'PING timeout: No response from {self.host}:{self.port} after {timeout}s'
) )
except socket.gaierror as e: except socket.gaierror as e:
raise VBANCMDConnectionError(f'Unable to resolve hostname {self.ip}') from e raise VBANCMDConnectionError(
f'Unable to resolve hostname {self.host}'
) from e
except Exception as e: except Exception as e:
raise VBANCMDConnectionError(f'PING failed: {e}') from e raise VBANCMDConnectionError(f'PING failed: {e}') from e
finally: finally:
@ -230,7 +232,7 @@ class VbanCmd(abc.ABC):
framecounter=self._get_next_framecounter(), framecounter=self._get_next_framecounter(),
payload=payload, payload=payload,
), ),
(socket.gethostbyname(self.ip), self.port), (socket.gethostbyname(self.host), self.port),
) )
def _set_rt(self, cmd: str, val: Union[str, float]): def _set_rt(self, cmd: str, val: Union[str, float]):
@ -238,6 +240,7 @@ class VbanCmd(abc.ABC):
self._send_request(f'{cmd}={val};') self._send_request(f'{cmd}={val};')
self.cache[cmd] = val self.cache[cmd] = val
@ratelimit
def sendtext(self, script) -> str | None: def sendtext(self, script) -> str | None:
"""Sends a multiple parameter string over a network.""" """Sends a multiple parameter string over a network."""
self._send_request(script) self._send_request(script)
@ -252,12 +255,10 @@ class VbanCmd(abc.ABC):
except TimeoutError as e: except TimeoutError as e:
self.logger.exception(f'Timeout waiting for matrix response: {e}') self.logger.exception(f'Timeout waiting for matrix response: {e}')
raise VBANCMDConnectionError( raise VBANCMDConnectionError(
f'Timeout waiting for response from {self.ip}:{self.port}' f'Timeout waiting for response from {self.host}:{self.port}'
) from e ) from e
return payload return payload
time.sleep(self.DELAY)
@property @property
def type(self) -> str: def type(self) -> str:
"""Returns the type of Voicemeeter installation.""" """Returns the type of Voicemeeter installation."""

View File

@ -27,7 +27,6 @@ class Subscriber(threading.Thread):
def run(self): def run(self):
while not self.stopped(): while not self.stopped():
try:
for nbs in NBS: for nbs in NBS:
sub_packet = VbanSubscribeHeader().to_bytes( sub_packet = VbanSubscribeHeader().to_bytes(
nbs, self._remote._get_next_framecounter() nbs, self._remote._get_next_framecounter()
@ -35,11 +34,6 @@ class Subscriber(threading.Thread):
self._remote.sock.sendto( self._remote.sock.sendto(
sub_packet, (self._remote.ip, self._remote.port) sub_packet, (self._remote.ip, self._remote.port)
) )
except TimeoutError as e:
self.logger.exception(f'{type(e).__name__}: {e}')
raise VBANCMDConnectionError(
f'timeout sending subscription to {self._remote.ip}:{self._remote.port}'
) from e
self.wait_until_stopped(10) self.wait_until_stopped(10)
self.logger.debug(f'terminating {self.name} thread') self.logger.debug(f'terminating {self.name} thread')
@ -128,7 +122,6 @@ class Producer(threading.Thread):
self.queue.put('pdirty') self.queue.put('pdirty')
if self._remote.event.ldirty: if self._remote.event.ldirty:
self.queue.put('ldirty') self.queue.put('ldirty')
# time.sleep(self._remote.ratelimit)
self.logger.debug(f'terminating {self.name} thread') self.logger.debug(f'terminating {self.name} thread')
self.queue.put(None) self.queue.put(None)