Skip to content

Commit

Permalink
feat: add menu to disconnect from server
Browse files Browse the repository at this point in the history
  • Loading branch information
thegamecracks committed Apr 26, 2024
1 parent 8821ea1 commit f42a04d
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 5 deletions.
31 changes: 27 additions & 4 deletions src/dumdum/client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import queue
import ssl
from tkinter import Event, Tk, messagebox
from tkinter import Event, Menu, Tk, messagebox
from tkinter.ttk import Frame
from typing import Any, Awaitable, ContextManager, Protocol, Type, runtime_checkable

Expand All @@ -17,6 +17,7 @@
from .errors import (
AuthenticationFailedError,
ClientCannotUpgradeSSLError,
DisconnectRequested,
ServerCannotUpgradeSSLError,
)
from .event_thread import EventThread
Expand Down Expand Up @@ -68,10 +69,12 @@ def __init__(
self._connect_lifetime_with_event_thread(event_thread)

self._client_events = queue.Queue()
self._disconnect_requested = asyncio.Event()
self._last_connection_exc = None

self.title("Dumdum Client")
self.geometry("900x600")
self.option_add("*tearOff", False)

self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
Expand All @@ -88,6 +91,12 @@ def switch_frame(self, frame: Frame) -> None:
self.frame = frame
self.frame.grid(sticky="nesw")

def switch_menu(self, menu: Menu | None) -> None:
if menu is None:
menu = Menu(self)

self.configure(menu=menu)

def submit(self, coro: Awaitable[Any]) -> concurrent.futures.Future:
fut = asyncio.run_coroutine_threadsafe(coro, self.event_thread.loop)
fut.add_done_callback(log_fut_exception)
Expand All @@ -105,9 +114,13 @@ async def attempt_connection(

coro = self._run_connection(host, port, ssl=ssl)
self._connection_task = asyncio.create_task(coro)
self._disconnect_requested.clear()

await self.client._wait_for_authentication()

def disconnect(self) -> None:
self.event_thread.loop.call_soon_threadsafe(self._disconnect_requested.set)

def _connect_lifetime_with_event_thread(self, event_thread: EventThread) -> None:
# In our application we'll be running an asyncio event loop in
# a separate thread. This event loop may try to run methods on
Expand All @@ -133,8 +146,10 @@ async def _run_connection(
ssl: ssl.SSLContext | None,
) -> None:
try:
async with self.client.connect(host, port, ssl=ssl):
await self.client.run_forever()
tg = asyncio.TaskGroup()
async with self.client.connect(host, port, ssl=ssl), tg:
tg.create_task(self.client.run_forever())
tg.create_task(self._wait_for_disconnect_request())
except BaseException as e:
self._last_connection_exc = e
else:
Expand All @@ -146,6 +161,10 @@ def _handle_event_threadsafe(self, event: ClientEvent):
self._client_events.put_nowait(event)
self.event_generate("<<ClientEvent>>")

async def _wait_for_disconnect_request(self) -> None:
await self._disconnect_requested.wait()
raise DisconnectRequested()

def _on_client_event(self, event: Event) -> None:
self._handle_event(self._client_events.get_nowait())

Expand All @@ -166,9 +185,10 @@ def _handle_event(self, event: ClientEvent) -> None:
messagebox.showerror("Authentication Interrupted", message)
return

from .chat_frame import ChatFrame
from .chat_frame import ChatFrame, ChatMenu

self.switch_frame(ChatFrame(self))
self.switch_menu(ChatMenu(self))
self.submit(self.client.list_channels())

if isinstance(self.frame, Dispachable):
Expand Down Expand Up @@ -210,6 +230,8 @@ def _on_connection_lost(self, event: Event) -> None:
"connect, you must download their certificate from a trusted "
"source and then specify it in the certificate field.",
)
elif has_exception(exc, DisconnectRequested):
log.info("Disconnected from server by client")
elif isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1:
first_exception = exc.exceptions[0]
log.error("Lost connection with server", exc_info=exc)
Expand All @@ -229,6 +251,7 @@ def _on_connection_lost(self, event: Event) -> None:

if not isinstance(self.frame, ConnectFrame):
self.switch_frame(ConnectFrame(self))
self.switch_menu(None)


def log_fut_exception(fut: concurrent.futures.Future) -> None:
Expand Down
17 changes: 16 additions & 1 deletion src/dumdum/client/chat_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import collections
import concurrent.futures
import time
from tkinter import Event, StringVar
from tkinter import Event, Menu, StringVar
from tkinter.ttk import Button, Entry, Frame, Label, Treeview
from typing import ContextManager

Expand Down Expand Up @@ -255,3 +255,18 @@ def do_send(self) -> None:
self.content_entry_var.set("")
coro = self.parent.app.client.send_message(channel.name, content)
self.parent.app.submit(coro)


class ChatMenu(Menu):
def __init__(self, parent: TkApp) -> None:
super().__init__(parent)

self.parent = parent

self.connection = Menu(self)
self.connection.add_command(label="Disconnect", command=self.do_disconnect)

self.add_cascade(label="Connection", menu=self.connection)

def do_disconnect(self) -> None:
self.parent.disconnect()
4 changes: 4 additions & 0 deletions src/dumdum/client/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ class ServerCannotUpgradeSSLError(Exception):

class AuthenticationFailedError(Exception):
"""Raised when authentication with the dumdum server fails."""


class DisconnectRequested(Exception):
"""Raised when the current connection should be disconnected."""

0 comments on commit f42a04d

Please sign in to comment.