Repos / mcross / 10b5974908
commit 10b5974908270d84bd21a240d5a74a845f586319
Author: Bùi Thành Nhân <hi@imnhan.com>
Date: Sun May 17 01:47:26 2020 +0700
run tkinter via curio instead - we async now!
diff --git a/poetry.lock b/poetry.lock
index c250d42..4262ef3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -25,6 +25,17 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.3"
+[[package]]
+category = "main"
+description = "Curio"
+name = "curio"
+optional = false
+python-versions = ">= 3.6"
+version = "1.2"
+
+[package.extras]
+test = ["pytest", "sphinx"]
+
[[package]]
category = "dev"
description = "Decorators for Humans"
@@ -298,7 +309,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[metadata]
-content-hash = "aa819a24de25e1f54368130e31d3524825aea37a1de2632a1eabd6cb6bafaa42"
+content-hash = "6aa72780c0ec9d3ed80370f35c09d1b1915c49675ab97732c49e2af47c5aa08f"
python-versions = "^3.7"
[metadata.files]
@@ -314,6 +325,9 @@ colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
]
+curio = [
+ {file = "curio-1.2.tar.gz", hash = "sha256:90f320fafb3f5b791f25ffafa7b561cc980376de173afd575a2114380de7939b"},
+]
decorator = [
{file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"},
{file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"},
diff --git a/pyproject.toml b/pyproject.toml
index 9bd58a8..9380af9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,6 +13,7 @@ mcross = "mcross:run"
[tool.poetry.dependencies]
python = "^3.7"
+curio = "^1.2"
[tool.poetry.dev-dependencies]
python-language-server = "^0.31.10"
diff --git a/src/mcross/gui/controller.py b/src/mcross/gui/controller.py
index 6bcaefb..03acb19 100644
--- a/src/mcross/gui/controller.py
+++ b/src/mcross/gui/controller.py
@@ -1,5 +1,7 @@
from ssl import SSLCertVerificationError
-from tkinter import Tk, messagebox
+from tkinter import TclError, Tk, messagebox
+
+import curio
from ..transport import (
GeminiUrl,
@@ -16,27 +18,54 @@ def __init__(self):
self.root = Tk()
self.model = Model()
self.view = View(self.root, self.model)
- self.view.go_callback = self.go_callback
- self.view.link_click_callback = self.link_click_callback
- self.view.back_callback = self.back_callback
- self.view.forward_callback = self.forward_callback
-
- def run(self):
self.root.title("McRoss Browser")
self.root.geometry("800x600")
- self.root.mainloop()
- def go_callback(self, url: str):
+ # Coroutine magic follows:
+
+ self.pending_coros = []
+
+ def schedule_as_coro(func):
+ return lambda *args: self.pending_coros.append(curio.spawn(func, *args))
+
+ self.view.go_callback = schedule_as_coro(self.go_callback)
+ self.view.link_click_callback = schedule_as_coro(self.link_click_callback)
+ self.view.back_callback = schedule_as_coro(self.back_callback)
+ self.view.forward_callback = schedule_as_coro(self.forward_callback)
+
+ def run(self):
+ # Instead of running tkinter's root.mainloop() directly,
+ # we rely on curio's event loop instead.
+ # The main() coroutine does these things in an infinite loop:
+ # - do tk's necessary GUI with root.update()
+ # - run pending coroutines if there's any. This is used to run callbacks
+ # triggered by the view.
+ # - sleep a little so we don't loop root.update() too quickly.
+ async def main():
+ try:
+ while True:
+ self.root.update()
+ for coroutine in self.pending_coros:
+ await coroutine
+ self.pending_coros = []
+ await curio.sleep(0.05)
+ except TclError as e:
+ if "application has been destroyed" not in str(e):
+ raise
+
+ curio.run(main)
+
+ async def go_callback(self, url: str):
# TODO more visual indications
url = GeminiUrl.parse_absolute_url(url)
- self.visit_link(url)
+ await self.visit_link(url)
- def link_click_callback(self, raw_url):
+ async def link_click_callback(self, raw_url):
# FIXME ugh
try:
url = GeminiUrl.parse(raw_url, self.model.history.get_current_url())
- self.visit_link(url)
+ await self.visit_link(url)
except NonAbsoluteUrlWithoutContextError:
messagebox.showwarning(
"Ambiguous link",
@@ -52,24 +81,24 @@ def link_click_callback(self, raw_url):
"Server is NOT using a valid CA-approved TLS certificate.",
)
- def visit_link(self, url: GeminiUrl):
- resp = self.load_page(url)
+ async def visit_link(self, url: GeminiUrl):
+ resp = await self.load_page(url)
self.model.history.visit(resp.url)
self.view.render_page()
- def back_callback(self):
+ async def back_callback(self):
self.model.history.go_back()
- self.load_page(self.model.history.get_current_url())
+ await self.load_page(self.model.history.get_current_url())
self.view.render_page()
- def forward_callback(self):
+ async def forward_callback(self):
self.model.history.go_forward()
- self.load_page(self.model.history.get_current_url())
+ await self.load_page(self.model.history.get_current_url())
self.view.render_page()
- def load_page(self, url: GeminiUrl):
+ async def load_page(self, url: GeminiUrl):
print("Requesting", url)
- resp = get(url)
+ resp = await get(url)
print("Received", resp)
if resp.status.startswith("2"):
diff --git a/src/mcross/transport.py b/src/mcross/transport.py
index ae103f5..0af07a0 100644
--- a/src/mcross/transport.py
+++ b/src/mcross/transport.py
@@ -1,8 +1,8 @@
import re
-import socket
-import ssl
from urllib.parse import urlparse
+import curio
+
MAX_RESP_HEADER_BYTES = 2 + 1 + 1024 + 2 # <STATUS><whitespace><META><CR><LF>
MAX_RESP_BODY_BYTES = 1024 * 1024 * 5
MAX_REDIRECTS = 3
@@ -102,32 +102,33 @@ def parse_absolute_url(text):
return GeminiUrl(parsed.hostname, parsed.port or 1965, parsed.path)
-def raw_get(url: GeminiUrl):
- context = ssl.create_default_context()
- with socket.create_connection((url.host, url.port)) as sock:
- with context.wrap_socket(sock, server_hostname=url.host) as ssock:
- ssock.send(f"gemini://{url.host}{url.path}\r\n".encode())
- header = ssock.recv(MAX_RESP_HEADER_BYTES).decode()
- status, meta = _parse_resp_header(header)
- resp = Response(status=status, meta=meta, url=url)
-
- if status.startswith("2"):
- body = b""
- msg = ssock.recv(4096)
+async def raw_get(url: GeminiUrl):
+ sock = await curio.open_connection(
+ url.host, url.port, ssl=True, server_hostname=url.host
+ )
+ async with sock:
+ await sock.sendall(f"gemini://{url.host}{url.path}\r\n".encode())
+ header = (await sock.recv(MAX_RESP_HEADER_BYTES)).decode()
+ status, meta = _parse_resp_header(header)
+ resp = Response(status=status, meta=meta, url=url)
+
+ if status.startswith("2"):
+ body = b""
+ msg = await sock.recv(4096)
+ body += msg
+ while msg and len(body) <= MAX_RESP_BODY_BYTES:
+ msg = await sock.recv(4096)
body += msg
- while msg and len(body) <= MAX_RESP_BODY_BYTES:
- msg = ssock.recv(4096)
- body += msg
- resp.body = body
+ resp.body = body
- return resp
+ return resp
-def get(url: GeminiUrl, redirect_count=0):
- resp = raw_get(url)
+async def get(url: GeminiUrl, redirect_count=0):
+ resp = await raw_get(url)
if resp.status.startswith("3") and redirect_count < MAX_REDIRECTS:
redirect_count += 1
new_url = GeminiUrl.parse_absolute_url(resp.meta)
print(f"Redirecting to {new_url}, count={redirect_count}")
- return get(new_url, redirect_count)
+ return await get(new_url, redirect_count)
return resp