Control socket: progress
このコミットが含まれているのは:
コミット
588ecc4c02
|
@ -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()
|
||||||
|
|
読み込み中…
新しいイシューから参照