Skip to content

Commit de0dab3

Browse files
authored
Merge pull request #24 from briangu/codex/fix-web-server-not-launching-in-repl
Fix web server startup from REPL
2 parents c06f177 + 6eec6f2 commit de0dab3

File tree

3 files changed

+137
-2
lines changed

3 files changed

+137
-2
lines changed

klongpy/repl.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import asyncio
2+
import threading
3+
import time
4+
import os
5+
import importlib.resources
6+
7+
from . import KlongInterpreter
8+
from .utils import CallbackEvent
9+
10+
11+
def start_loop(loop: asyncio.AbstractEventLoop, stop_event: asyncio.Event) -> None:
12+
asyncio.set_event_loop(loop)
13+
loop.run_until_complete(stop_event.wait())
14+
15+
16+
def setup_async_loop(debug: bool = False, slow_callback_duration: float = 86400.0):
17+
loop = asyncio.new_event_loop()
18+
loop.slow_callback_duration = slow_callback_duration
19+
if debug:
20+
loop.set_debug(True)
21+
stop_event = asyncio.Event()
22+
thread = threading.Thread(target=start_loop, args=(loop, stop_event), daemon=True)
23+
thread.start()
24+
return loop, thread, stop_event
25+
26+
27+
def cleanup_async_loop(loop: asyncio.AbstractEventLoop, loop_thread: threading.Thread, stop_event: asyncio.Event, debug: bool = False, name: str | None = None) -> None:
28+
if loop.is_closed():
29+
return
30+
31+
loop.call_soon_threadsafe(stop_event.set)
32+
loop_thread.join()
33+
34+
pending_tasks = asyncio.all_tasks(loop=loop)
35+
if len(pending_tasks) > 0:
36+
if name:
37+
print(f"WARNING: pending tasks in {name} loop")
38+
for task in pending_tasks:
39+
loop.call_soon_threadsafe(task.cancel)
40+
while len(asyncio.all_tasks(loop=loop)) > 0:
41+
time.sleep(0)
42+
43+
loop.stop()
44+
45+
if not loop.is_closed():
46+
loop.close()
47+
48+
49+
def append_pkg_resource_path_KLONGPATH() -> None:
50+
with importlib.resources.as_file(importlib.resources.files('klongpy')) as pkg_path:
51+
pkg_lib_path = os.path.join(pkg_path, 'lib')
52+
klongpath = os.environ.get('KLONGPATH', '.:lib')
53+
klongpath = f"{klongpath}:{pkg_lib_path}" if klongpath else str(pkg_lib_path)
54+
os.environ['KLONGPATH'] = klongpath
55+
56+
57+
def create_repl(debug: bool = False):
58+
io_loop, io_thread, io_stop = setup_async_loop(debug=debug)
59+
klong_loop, klong_thread, klong_stop = setup_async_loop(debug=debug)
60+
61+
append_pkg_resource_path_KLONGPATH()
62+
63+
klong = KlongInterpreter()
64+
shutdown_event = CallbackEvent()
65+
klong['.system'] = {'ioloop': io_loop, 'klongloop': klong_loop, 'closeEvent': shutdown_event}
66+
67+
return klong, (io_loop, io_thread, io_stop, klong_loop, klong_thread, klong_stop)
68+
69+
70+
def cleanup_repl(loops, debug: bool = False) -> None:
71+
io_loop, io_thread, io_stop, klong_loop, klong_thread, klong_stop = loops
72+
cleanup_async_loop(io_loop, io_thread, io_stop, debug=debug, name='io_loop')
73+
cleanup_async_loop(klong_loop, klong_thread, klong_stop, debug=debug, name='klong_loop')

klongpy/web/sys_fn_web.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import logging
22
import sys
3+
import asyncio
4+
import concurrent.futures
35

46
from aiohttp import web
57

@@ -113,7 +115,17 @@ async def start_server():
113115
site = web.TCPSite(runner, bind, port)
114116
await site.start()
115117

116-
server_task = klong['.system']['ioloop'].create_task(start_server())
118+
# create the server task in the ioloop thread and capture the task handle
119+
server_loop = klong['.system']['ioloop']
120+
task_future = concurrent.futures.Future()
121+
122+
def _start():
123+
task = asyncio.create_task(start_server())
124+
task_future.set_result(task)
125+
126+
server_loop.call_soon_threadsafe(_start)
127+
server_task = task_future.result()
128+
117129
return WebServerHandle(bind, port, runner, server_task)
118130

119131

@@ -129,7 +141,7 @@ def eval_sys_fn_shutdown_web_server(klong, x):
129141
x = x.a.fn
130142
if isinstance(x, WebServerHandle) and x.runner is not None:
131143
print("shutting down web server")
132-
klong['.system']['ioloop'].run_until_complete(x.shutdown())
144+
asyncio.run_coroutine_threadsafe(x.shutdown(), klong['.system']['ioloop']).result()
133145
return 1
134146
return 0
135147

tests/test_sys_fn_web.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import asyncio
2+
import socket
3+
import unittest
4+
5+
import aiohttp
6+
7+
from klongpy.repl import create_repl, cleanup_repl
8+
9+
10+
class TestSysFnWeb(unittest.TestCase):
11+
def setUp(self):
12+
self.klong, self.loops = create_repl()
13+
(self.ioloop, self.ioloop_thread, self.io_stop,
14+
self.klongloop, self.klongloop_thread, self.klong_stop) = self.loops
15+
self.handle = None
16+
17+
def tearDown(self):
18+
if self.handle is not None and self.handle.task is not None:
19+
asyncio.run_coroutine_threadsafe(self.handle.shutdown(), self.ioloop).result()
20+
cleanup_repl(self.loops)
21+
22+
def _free_port(self):
23+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
24+
s.bind(("", 0))
25+
port = s.getsockname()[1]
26+
s.close()
27+
return port
28+
29+
def test_web_server_start_and_stop(self):
30+
klong = self.klong
31+
port = self._free_port()
32+
33+
klong('.py("klongpy.web")')
34+
klong('index::{x;"hello"}')
35+
klong('get:::{}')
36+
klong('get,"/",index')
37+
klong('post:::{}')
38+
handle = klong(f'h::.web({port};get;post)')
39+
self.handle = handle
40+
41+
async def fetch():
42+
async with aiohttp.ClientSession() as session:
43+
async with session.get(f"http://localhost:{port}/") as resp:
44+
return await resp.text()
45+
46+
response = asyncio.run_coroutine_threadsafe(fetch(), self.ioloop).result()
47+
self.assertEqual(response, "hello")
48+
49+
asyncio.run_coroutine_threadsafe(handle.shutdown(), self.ioloop).result()
50+

0 commit comments

Comments
 (0)