Skip to content

Commit

Permalink
appservice: Add initial status API
Browse files Browse the repository at this point in the history
Track session status. Right after creation, it starts as `wait_target`.
After the target machine has connected (first connection to the /ws
path), it changes to `running`. We'll probably add more states in the
future.

Add /sessions/ID/status API to query it, so that e.g. the target machine
and hte UI can poll this to know when to connect.

Fixes #28
  • Loading branch information
martinpitt committed Sep 19, 2022
1 parent 216ff8f commit babd504
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 5 deletions.
25 changes: 20 additions & 5 deletions appservice/multiplexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
SESSION_INSTANCE_DOMAIN = os.getenv('SESSION_INSTANCE_DOMAIN', '')

PODMAN_SOCKET = '/run/podman/podman.sock'
# states: wait_target or running
SESSIONS = {}
REDIS = redis.Redis(host=os.environ['REDIS_SERVICE_HOST'], port=int(os.environ.get('REDIS_SERVICE_PORT', '6379')))

Expand Down Expand Up @@ -85,7 +86,6 @@ async def new_session_podman(self, sessionid):
return status, content

async def handle_session_new(self, request):
global SESSIONS
sessionid = str(uuid.uuid4())
assert sessionid not in SESSIONS

Expand All @@ -97,15 +97,19 @@ async def handle_session_new(self, request):

if pod_status >= 200 and pod_status < 300:
response = web.Response(status=200, text=json.dumps({'id': sessionid}))
SESSIONS[sessionid] = True
dumped_sessions = json.dumps(SESSIONS)
await REDIS.set('sessions', dumped_sessions)
await REDIS.publish('sessions', dumped_sessions)
await update_session(sessionid, 'wait_target')
else:
response = web.Response(status=pod_status, text=f'creating session container failed: {content}')

return response

async def handle_session_status(self, upstream_req):
sessionid = upstream_req.match_info['sessionid']
try:
return web.Response(text=SESSIONS[sessionid])
except KeyError:
return web.HTTPNotFound(text='unknown session ID')

async def handle_session_id(self, upstream_req):
sessionid = upstream_req.match_info['sessionid']
if sessionid not in SESSIONS:
Expand All @@ -116,6 +120,8 @@ async def handle_session_id(self, upstream_req):
target_url = f'http://session-{sessionid}{SESSION_INSTANCE_DOMAIN}:9090{upstream_req.path_qs}'
elif path == 'ws':
target_url = f'http://session-{sessionid}{SESSION_INSTANCE_DOMAIN}:8080{upstream_req.path_qs}'
if SESSIONS[sessionid] == 'wait_target':
await update_session(sessionid, 'running')
else:
return web.HTTPNotFound(text=f'invalid session path prefix: {path}')

Expand Down Expand Up @@ -191,6 +197,14 @@ async def init_sessions():
logger.debug('initial sessions: %s', SESSIONS)


async def update_session(session_id, status):
global SESSIONS
SESSIONS[session_id] = status
dumped_sessions = json.dumps(SESSIONS)
await REDIS.set('sessions', dumped_sessions)
await REDIS.publish('sessions', dumped_sessions)


async def watch_redis(channel):
global SESSIONS
while True:
Expand Down Expand Up @@ -220,6 +234,7 @@ async def main():
app = web.Application()
app.router.add_route('GET', f'{config.ROUTE_API}/ping', handlers.handle_ping)
app.router.add_route('GET', f'{config.ROUTE_API}/sessions/new', handlers.handle_session_new)
app.router.add_route('GET', f'{config.ROUTE_API}/sessions/{{sessionid}}/status', handlers.handle_session_status)
app.router.add_route('*', f'{config.ROUTE_WSS}/sessions/{{sessionid}}/{{path:.*}}', handlers.handle_session_id)

runner = web.AppRunner(app, auto_decompress=False)
Expand Down
21 changes: 21 additions & 0 deletions test/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ def newSession(self):
sessionid = json.load(response)['id']
self.assertIsInstance(sessionid, str)

# inital status
response = self.request(f'{self.api_url}{config.ROUTE_API}/sessions/{sessionid}/status')
self.assertEqual(response.status, 200)
self.assertEqual(response.read(), b'wait_target')

# API URL is on the container host's localhost; translate for the container DNS
websocket_url = self.api_url.replace('localhost', 'host.containers.internal').replace('https:', 'wss:')
podman = ['podman', 'run', '-d', '--pod', 'webconsoleapp',
Expand All @@ -112,6 +117,17 @@ def newSession(self):

subprocess.check_call(podman + cmd)

# successful bridge connection updates status
for retry in range(10):
response = self.request(f'{self.api_url}{config.ROUTE_API}/sessions/{sessionid}/status')
self.assertEqual(response.status, 200)
status = response.read()
if status == b'running':
break
time.sleep(0.5)
else:
self.fail(f'session status was not updated to running, still at {status}')

return sessionid

def checkSession(self, sessionid):
Expand Down Expand Up @@ -141,6 +157,11 @@ def testSessions(self):
# first session still works
self.checkSession(s1)

# unknown session ID
with self.assertRaises(urllib.error.HTTPError) as cm:
self.request(f'{self.api_url}{config.ROUTE_API}/sessions/123unknown/status')
self.assertEqual(cm.exception.code, 404)

# crash container for s2; use --time 0 once we have podman 4.0 everywhere
subprocess.check_call(['podman', 'rm', '--force', f'session-{s2}'])
# first session still works
Expand Down

0 comments on commit babd504

Please sign in to comment.