Control socket: progress

このコミットが含まれているのは:
n9k 2022-06-13 03:46:02 +00:00
コミット 588ecc4c02
2個のファイルの変更194行の追加82行の削除

ファイルの表示

@ -1,125 +1,233 @@
import asyncio import asyncio
import json import json
from anonstream.stream import get_stream_title from anonstream.stream import get_stream_title, set_stream_title
class UnknownMethod(Exception):
pass
class UnknownCommand(Exception):
pass
class UnknownArgument(Exception):
pass
class BadArgument(Exception):
pass
class Incomplete(Exception):
pass
class Garbage(Exception):
pass
class Failed(Exception):
pass
class Exit(Exception):
pass
def start_control_server_at(address): def start_control_server_at(address):
return asyncio.start_unix_server( return asyncio.start_unix_server(serve_control_client, address)
handle_control_client,
address,
)
async def handle_control_client(reader, writer): async def serve_control_client(reader, writer):
while line := await reader.readline(): while line := await reader.readline():
try: try:
request = line.decode('utf-8') request = line.decode('utf-8')
except UnicodeDecodeError as e: except UnicodeDecodeError as e:
response = f'error: {e}' normal, response = None, str(e)
else: else:
method, args, normal, response = await parse(request) try:
if method is None: normal, response = await parse_request(request)
pass except Exit:
elif normal is not None: writer.close()
writer.write(f'{normal}\n'.encode()) break
elif response is not None:
writer.write(f'error: '.encode())
if response is not None:
writer.write(response.encode())
await writer.drain()
else:
writer.close()
break
async def parse(request): if normal is not None:
normal_method, normal_options = normal
if normal_method is not None:
writer.write(normal_method.encode())
for arg in normal_options:
writer.write(b' ')
writer.write(arg.encode())
writer.write(b'\n')
elif response:
writer.write(b'error: ')
writer.write(response.encode())
await writer.drain()
async def parse_request(request):
try: try:
method, *args = request.split() method, *options = request.split()
except ValueError: except ValueError:
method, args, normal, response = None, [], None, '' normal, response = (None, []), ''
else: else:
match method: try:
case 'help': normal, response = await parse(method, options)
normal_args, response = await parse_help(args) except UnknownMethod as e:
case 'exit': unknown_method, *_ = e.args
normal_args, response = await parse_exit(args)
case 'title':
normal_args, response = await parse_title(args)
case _:
normal_args = None
response = f"method {method!r} is unknown, try 'help'\n"
if normal_args is None:
normal = None normal = None
if response is None: response = f"method {unknown_method!r} is unknown, try 'help'\n"
response = f"command {args[0]!r} is unknown, try {f'{method} help'!r}\n" except UnknownCommand as e:
elif len(normal_args) == 0: method, unknown_command, *_ = e.args
normal = method normal = None
else: response = f"command {unknown_command!r} is unknown, try {f'{method} help'!r}\n"
normal = f'{method} {" ".join(normal_args)}' or method except BadArgument as e:
return method, args, normal, response reason, *_ = e.args
normal = None
response = f"{reason}, try {f'{method} help'!r}\n"
except Incomplete as e:
method, *_ = e.args
normal = None
response = f"command is incomplete, try {f'{method} help'!r}\n"
except Garbage as e:
garbage, *_ = e.args
normal = None
response = f"command has trailing garbage {garbage!r}, try {f'{method} help'!r}\n"
except Failed as e:
reason, *_ = e.args
normal = None
response = reason + '\n'
return normal, response
async def parse_help(args): async def parse(method, options):
try:
command, *args = options
except ValueError:
command, args = None, []
try:
functions = METHOD_COMMAND_FUNCTIONS[method]
except KeyError:
raise UnknownMethod(method)
else:
normal_method = method
try:
fn = functions[command]
except KeyError:
raise UnknownCommand(method, command)
else:
try:
normal_options, response = await fn(args)
except Incomplete as e:
raise Incomplete(method) from e
normal = (normal_method, normal_options)
return normal, response
async def command_help(args):
match args: match args:
case []: case []:
normal_args = [] normal_options = []
response = ( response = (
'Usage: METHOD {COMMAND | help}\n' 'Usage: METHOD [COMMAND | help]\n'
'Examples:\n' 'Examples:\n'
' help.......................show this help message\n' ' help.......................show this help message\n'
' exit.......................close the connection\n' ' exit.......................close the connection\n'
' title [show [CODEC]].......show the stream title\n' ' title [show]...............show the stream title\n'
' title set TITLE............set the stream title\n' ' title set TITLE............set the stream title\n'
' user [show]................show a list of users\n' ' user [show]................show a list of users\n'
' user set USER ATTR VALUE...set an attribute of a user\n' ' user set USER ATTR VALUE...set an attribute of a user\n'
) )
case ['help']: case [*garbage]:
normal_args = ['help'] raise Garbage(garbage)
return normal_options, response
async def command_help_help(args):
match args:
case []:
normal_options = ['help']
response = ( response = (
'Usage: help\n' 'Usage: help\n'
'show usage syntax and examples\n' 'show usage syntax and examples\n'
) )
case _: case [*garbage]:
normal_args = None raise Garbage(garbage)
response = None return normal_options, response
return normal_args, response
async def parse_exit(args): async def command_exit(args):
match args: match args:
case []: case []:
normal_args = [] raise Exit
response = None case [*garbage]:
case ['help']: raise Garbage(garbage)
normal_args = ['help']
async def command_exit_help(args):
match args:
case []:
normal_options = ['help']
response = ( response = (
'Usage: {exit | quit}\n' 'Usage: exit\n'
'close the connection\n' 'close the connection\n'
) )
case _: case [*garbage]:
normal_args = None raise Garbage(garbage)
response = None return normal_options, response
return normal_args, response
async def parse_title(args): async def command_title_help(args):
match args: match args:
case [] | ['show'] | ['show', 'json']: case []:
normal_args = ['show', 'json'] normal_options = ['help']
response = json.dumps(await get_stream_title()) + '\n'
case ['show', 'utf-8']:
normal_args = ['show']
response = await get_stream_title() + '\n'
case ['show', arg, *_]:
normal_args = None
response = f"option {arg!r} is unknown, try 'title help'\n"
case ['help']:
normal_args = ['help']
response = ( response = (
'Usage: title {show [CODEC] | set TITLE}\n' 'Usage: title [show | set TITLE]\n'
'Commands:\n' 'Commands:\n'
' title show [CODEC].....show the stream title\n' ' title [show].......show the stream title\n'
' title set TITLE........set the stream title to TITLE collapsing whitespace\n' ' title set TITLE....set the stream title to TITLE\n'
'Arguments:\n' 'Arguments:\n'
' CODEC..................=[utf-8 | json]\n' ' TITLE..............a json-encoded string, whitespace must be \\uXXXX-escaped\n'
' TITLE..................a UTF-8-encoded string\n'
) )
case _: case [*garbage]:
normal_args = None raise Garbage(garbage)
response = None return normal_options, response
return normal_args, response
async def command_title_show(args):
match args:
case []:
normal_options = ['show']
response = json.dumps(await get_stream_title()) + '\n'
case [*garbage]:
raise Garbage(garbage)
return normal_options, response
async def command_title_set(args):
match args:
case [title_json]:
try:
title = json.loads(title_json)
except json.JSONDecodeError as e:
raise BadArgument('could not decode json')
else:
if not isinstance(title, str):
raise BadArgument('could not decode json as string')
else:
try:
await set_stream_title(title)
except OSError as e:
raise Failed(str(e)) from e
normal_options = ['set', json.dumps(title).replace(' ', r'\u0020')]
response = ''
case []:
raise Incomplete
case [_, *garbage]:
raise Garbage(garbage)
return normal_options, response
METHOD_HELP = 'help'
METHOD_EXIT = 'exit'
METHOD_TITLE = 'title'
METHOD_COMMAND_FUNCTIONS = {
METHOD_HELP: {
None: command_help,
'help': command_help_help,
},
METHOD_EXIT: {
None: command_exit,
'help': command_exit_help,
},
METHOD_TITLE: {
None: command_title_show,
'help': command_title_help,
'show': command_title_show,
'set': command_title_set,
},
}

ファイルの表示

@ -24,6 +24,10 @@ async def get_stream_title():
title = '' title = ''
return title return title
async def set_stream_title(title):
async with aiofiles.open(CONFIG['STREAM_TITLE'], 'w') as fp:
await fp.write(title)
def get_stream_uptime(rounded=True): def get_stream_uptime(rounded=True):
try: try:
playlist, mtime = get_playlist() playlist, mtime = get_playlist()