mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-27 03:01:52 +01:00
149 lines
6.4 KiB
Python
149 lines
6.4 KiB
Python
"""Simple Timer lifecycle through actual Decky instance dispatch, never Plugin()."""
|
|
import asyncio
|
|
import json
|
|
import time
|
|
import urllib.request
|
|
|
|
import websockets
|
|
|
|
|
|
class Client:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.sequence = 0
|
|
self.events = []
|
|
|
|
async def __aenter__(self):
|
|
token = urllib.request.urlopen('http://127.0.0.1:1337/auth/token', timeout=2).read().decode()
|
|
self.ws = await websockets.connect('ws://127.0.0.1:1337/ws?auth='+token)
|
|
return self
|
|
|
|
async def __aexit__(self, *_):
|
|
await self.ws.close()
|
|
|
|
async def receive(self, timeout=8):
|
|
response = json.loads(await asyncio.wait_for(self.ws.recv(), timeout))
|
|
if response.get('type') == 5 and response.get('event') == 'loader/plugin_event':
|
|
event = response['args'][0]
|
|
if event['plugin'] == self.name:
|
|
self.events.append(event)
|
|
return response
|
|
|
|
async def call(self, method, *args, success=True):
|
|
self.sequence += 1
|
|
await self.ws.send(json.dumps({'type': 0, 'id': self.sequence,
|
|
'route': 'loader/call_plugin_method', 'args': [self.name, method, *args]}))
|
|
while True:
|
|
response = await self.receive()
|
|
if response.get('id') != self.sequence:
|
|
continue
|
|
await self.ws.send(json.dumps({'type': 3, 'id': self.sequence}))
|
|
if success:
|
|
assert response['type'] == 1, response
|
|
return response['result']
|
|
return response
|
|
|
|
async def seconds(self):
|
|
offset = len(self.events)
|
|
await self.call('load_remaining_seconds')
|
|
while not any(e['event'] == 'simple_timer_seconds_updated' for e in self.events[offset:]):
|
|
await self.receive()
|
|
return next(e['args'][0] for e in reversed(self.events)
|
|
if e['event'] == 'simple_timer_seconds_updated')
|
|
|
|
async def drain(self, seconds):
|
|
deadline = time.monotonic()+seconds
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
await self.receive(max(.01, deadline-time.monotonic()))
|
|
except TimeoutError:
|
|
break
|
|
|
|
|
|
async def initial(name, negative):
|
|
async with Client(name) as client:
|
|
await client.call('set_subtle_mode', True) # No sound or physical actions.
|
|
response = await client.call('cancel_timer', success=False)
|
|
if negative:
|
|
assert response['type'] == -1 and 'timer_task' in str(response), response
|
|
else:
|
|
assert response['type'] == 1, response
|
|
await client.call('start_timer', 60)
|
|
end = await client.call('settings_getSetting', 'timer_end', None)
|
|
assert end > time.time()
|
|
await client.call('cancel_timer')
|
|
saved = await client.call('settings_getSetting', 'timer_end', None)
|
|
assert (saved is not None) if negative else (saved is None), saved
|
|
await client.call('start_timer', 1)
|
|
await client.call('start_timer', 60)
|
|
await client.drain(6.5) # Past upstream's 5-second tick and first timer's expiry.
|
|
completed = [e for e in client.events if e['event'] == 'simple_timer_event']
|
|
if negative:
|
|
assert completed, client.events
|
|
else:
|
|
assert not completed, completed
|
|
assert await client.seconds() > 45
|
|
await client.call('cancel_timer')
|
|
if negative:
|
|
return {'freshCancelError': response, 'cancelRetainsEnd': True,
|
|
'replacedTimerStillFires': True}
|
|
await client.call('start_timer', 1)
|
|
client.events.clear()
|
|
await client.drain(5.5)
|
|
completed = [e for e in client.events if e['event'] == 'simple_timer_event']
|
|
assert len(completed) == 1 and completed[0]['args'] == ['Your session has ended!', True], completed
|
|
assert await client.seconds() == 0
|
|
assert await client.call('settings_getSetting', 'timer_end', None) is None
|
|
client.events.clear()
|
|
await client.call('start_timer', -20) # An already expired, private silent deadline.
|
|
await client.drain(.3)
|
|
expired = [e for e in client.events if e['event'] == 'simple_timer_event']
|
|
assert len(expired) == 1 and expired[0]['args'] == ['Your timer has expired!', True], expired
|
|
assert await client.call('settings_getSetting', 'timer_end', None) is None
|
|
await client.call('start_timer', 60)
|
|
return {'end': await client.call('settings_getSetting', 'timer_end', None),
|
|
'recents': await client.call('settings_getSetting', 'recent_timers_seconds', [])}
|
|
|
|
|
|
async def resumed(name, saved):
|
|
async with Client(name) as client:
|
|
assert 0 < await client.seconds() < 60
|
|
assert await client.call('settings_getSetting', 'timer_end', None) == saved['end']
|
|
assert await client.call('settings_getSetting', 'recent_timers_seconds', []) == saved['recents']
|
|
await client.call('cancel_timer')
|
|
assert await client.call('settings_getSetting', 'timer_end', None) is None
|
|
|
|
|
|
async def idle(name):
|
|
async with Client(name) as client:
|
|
assert await client.seconds() == 0
|
|
assert await client.call('settings_getSetting', 'timer_end', None) is None
|
|
await client.call('cancel_timer') # Idempotent before any new timer.
|
|
assert await client.call('settings_getSetting', 'subtle_mode', False) is True
|
|
|
|
|
|
def probe(name, restart, log, negative=False):
|
|
saved = asyncio.run(initial(name, negative))
|
|
if negative:
|
|
restart({'timer_end': time.time()-60, 'subtle_mode': True})
|
|
asyncio.run(read_after_startup(name))
|
|
assert "has no attribute 'timer_task'" in log.read_text(), log.read_text()[-6000:]
|
|
return saved | {'expiredRestartError': True}
|
|
restart()
|
|
asyncio.run(resumed(name, saved))
|
|
restart()
|
|
asyncio.run(idle(name))
|
|
restart({'timer_end': time.time()-60, 'subtle_mode': True})
|
|
asyncio.run(idle(name))
|
|
return {'freshCancel': True, 'replacementSuppressesOldCompletion': True,
|
|
'completionClearsEnd': True, 'lateExpiryEmitsWithoutSelfCancel': True,
|
|
'activeRestartPreservesDeadlineAndRecents': True,
|
|
'canceledRestartIdle': True, 'expiredRestartIdle': True}
|
|
|
|
|
|
async def read_after_startup(name):
|
|
async with Client(name) as client:
|
|
# _main is scheduled independently; allow it to finish before reading its journal.
|
|
await client.call('settings_getSetting', 'subtle_mode', False)
|
|
await client.drain(.3)
|