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):
defaultkwargs = {
'ip': 'localhost',
'host': 'localhost',
'port': 6980,
'streamname': 'Command1',
'bps': 256000,
'channel': 0,
'ratelimit': 0.01,
'timeout': 5,
'script_ratelimit': 0.05, # 20 commands per second, to avoid overloading Voicemeeter
'timeout': 5, # timeout on socket operations, in seconds
'disable_rt_listeners': False,
'sync': False,
'pdirty': False,
'ldirty': False,
}
if 'ip' in kwargs:
defaultkwargs['host'] = kwargs.pop('ip') # for backwards compatibility
if 'subs' in kwargs:
defaultkwargs |= kwargs.pop('subs') # for backwards compatibility
kwargs = defaultkwargs | kwargs

View File

@ -1,6 +1,23 @@
import time
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):
"""Check cache for a bool prop"""

View File

@ -17,7 +17,7 @@ from .packet.headers import (
)
from .packet.ping0 import VbanPing0Payload, VbanServerType
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
logger = logging.getLogger(__name__)
@ -38,7 +38,7 @@ class VbanCmd(abc.ABC):
def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__)
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()
for attr, val in kwargs.items():
setattr(self, attr, val)
@ -52,9 +52,9 @@ class VbanCmd(abc.ABC):
self.cache = {}
self._pdirty = False
self._ldirty = False
self._script = str()
self.stop_event = None
self.producer = None
self._last_script_request_time = 0
@abc.abstractmethod
def __str__(self):
@ -113,7 +113,7 @@ class VbanCmd(abc.ABC):
self.producer.start()
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__
)
)
@ -149,8 +149,8 @@ class VbanCmd(abc.ABC):
self.sock.settimeout(0.5)
try:
self.sock.sendto(ping_packet, (socket.gethostbyname(self.ip), self.port))
self.logger.debug(f'PING sent to {self.ip}:{self.port}')
self.sock.sendto(ping_packet, (socket.gethostbyname(self.host), self.port))
self.logger.debug(f'PING sent to {self.host}:{self.port}')
start_time = time.time()
response_count = 0
@ -193,11 +193,13 @@ class VbanCmd(abc.ABC):
f'PING timeout after {timeout}s, received {response_count} non-PONG packets'
)
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:
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:
raise VBANCMDConnectionError(f'PING failed: {e}') from e
finally:
@ -230,7 +232,7 @@ class VbanCmd(abc.ABC):
framecounter=self._get_next_framecounter(),
payload=payload,
),
(socket.gethostbyname(self.ip), self.port),
(socket.gethostbyname(self.host), self.port),
)
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.cache[cmd] = val
@ratelimit
def sendtext(self, script) -> str | None:
"""Sends a multiple parameter string over a network."""
self._send_request(script)
@ -252,12 +255,10 @@ class VbanCmd(abc.ABC):
except TimeoutError as e:
self.logger.exception(f'Timeout waiting for matrix response: {e}')
raise VBANCMDConnectionError(
f'Timeout waiting for response from {self.ip}:{self.port}'
f'Timeout waiting for response from {self.host}:{self.port}'
) from e
return payload
time.sleep(self.DELAY)
@property
def type(self) -> str:
"""Returns the type of Voicemeeter installation."""

View File

@ -27,7 +27,6 @@ class Subscriber(threading.Thread):
def run(self):
while not self.stopped():
try:
for nbs in NBS:
sub_packet = VbanSubscribeHeader().to_bytes(
nbs, self._remote._get_next_framecounter()
@ -35,11 +34,6 @@ class Subscriber(threading.Thread):
self._remote.sock.sendto(
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.logger.debug(f'terminating {self.name} thread')
@ -128,7 +122,6 @@ class Producer(threading.Thread):
self.queue.put('pdirty')
if self._remote.event.ldirty:
self.queue.put('ldirty')
# time.sleep(self._remote.ratelimit)
self.logger.debug(f'terminating {self.name} thread')
self.queue.put(None)