Skip to content

Commit 0dd4664

Browse files
committed
Refactor shutdown procedure
1 parent ab14b76 commit 0dd4664

File tree

12 files changed

+216
-331
lines changed

12 files changed

+216
-331
lines changed

pyproject.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ version = "0.1.1"
44
description = "Text User Interface using Rich"
55
authors = ["Will McGugan <[email protected]>"]
66
license = "MIT"
7+
classifiers = [
8+
"Development Status :: 1 - Planning",
9+
"Environment :: Console",
10+
"Intended Audience :: Developers",
11+
"Operating System :: Microsoft :: Windows",
12+
"Operating System :: MacOS",
13+
"Operating System :: POSIX :: Linux",
14+
"Programming Language :: Python :: 3.7",
15+
"Programming Language :: Python :: 3.8",
16+
"Programming Language :: Python :: 3.9",
17+
"Programming Language :: Python :: 3.10",
18+
]
19+
720

821
[tool.poetry.dependencies]
922
python = "^3.7"

src/textual/_linux_driver.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,21 @@ def _patch_iflag(cls, attrs: int) -> int:
139139
| termios.IGNCR
140140
)
141141

142+
def disable_input(self) -> None:
143+
try:
144+
if not self.exit_event.is_set():
145+
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
146+
self._disable_mouse_support()
147+
self.exit_event.set()
148+
if self._key_thread is not None:
149+
self._key_thread.join()
150+
except Exception:
151+
log.exception("error in disable_input")
152+
142153
def stop_application_mode(self) -> None:
143154
log.debug("stop_application_mode()")
144155

145-
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
146-
147-
self._disable_mouse_support()
148-
self.exit_event.set()
149-
if self._key_thread is not None:
150-
self._key_thread.join()
156+
self.disable_input()
151157

152158
if self.attrs_before is not None:
153159
try:
@@ -190,12 +196,18 @@ def more_data() -> bool:
190196
read = os.read
191197

192198
log.debug("started key thread")
193-
while not self.exit_event.is_set():
194-
selector_events = selector.select(0.1)
195-
for _selector_key, mask in selector_events:
196-
unicode_data = decode(read(fileno, 1024))
197-
for event in parser.feed(unicode_data):
198-
send_event(event)
199+
try:
200+
while not self.exit_event.is_set():
201+
selector_events = selector.select(0.1)
202+
for _selector_key, mask in selector_events:
203+
if mask | selectors.EVENT_READ:
204+
unicode_data = decode(read(fileno, 1024))
205+
for event in parser.feed(unicode_data):
206+
send_event(event)
207+
except Exception:
208+
log.exception("error running key thread")
209+
finally:
210+
selector.close()
199211

200212

201213
if __name__ == "__main__":

src/textual/_timer.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import weakref
4-
from asyncio import Event, TimeoutError, wait_for
4+
from asyncio import CancelledError, Event, TimeoutError, wait_for
55
from time import monotonic
66
from typing import Awaitable, Callable
77

@@ -62,18 +62,21 @@ async def run(self) -> None:
6262
_interval = self._interval
6363
_wait = self._stop_event.wait
6464
start = monotonic()
65-
while _repeat is None or count <= _repeat:
66-
next_timer = start + (count * _interval)
67-
try:
68-
if await wait_for(_wait(), max(0, next_timer - monotonic())):
65+
try:
66+
while _repeat is None or count <= _repeat:
67+
next_timer = start + (count * _interval)
68+
try:
69+
if await wait_for(_wait(), max(0, next_timer - monotonic())):
70+
break
71+
except TimeoutError:
72+
pass
73+
event = events.Timer(
74+
self.sender, timer=self, count=count, callback=self._callback
75+
)
76+
try:
77+
await self.target.post_message(event)
78+
except EventTargetGone:
6979
break
70-
except TimeoutError:
71-
pass
72-
event = events.Timer(
73-
self.sender, timer=self, count=count, callback=self._callback
74-
)
75-
try:
76-
await self.target.post_message(event)
77-
except EventTargetGone:
78-
break
79-
count += 1
80+
count += 1
81+
except CancelledError:
82+
pass

src/textual/_types.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,12 @@
1616

1717

1818
class MessageTarget(Protocol):
19-
async def post_message(
20-
self,
21-
message: "Message",
22-
priority: Optional[int] = None,
23-
) -> bool:
19+
async def post_message(self, message: "Message") -> bool:
2420
...
2521

2622

2723
class EventTarget(Protocol):
28-
async def post_message(
29-
self,
30-
message: "Message",
31-
priority: Optional[int] = None,
32-
) -> bool:
24+
async def post_message(self, message: "Message") -> bool:
3325
...
3426

3527

src/textual/app.py

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,18 @@ def __init__(
5656
self,
5757
console: Console = None,
5858
screen: bool = True,
59-
driver: Type[Driver] = None,
59+
driver_class: Type[Driver] = None,
6060
view: View = None,
6161
title: str = "Megasoma Application",
6262
):
6363
super().__init__()
6464
self.console = console or get_console()
6565
self._screen = screen
66-
self.driver = driver or LinuxDriver
66+
self.driver_class = driver_class or LinuxDriver
6767
self.title = title
6868
self.view = view or LayoutView()
6969
self.children: set[MessagePump] = set()
70+
self._driver: Driver | None = None
7071

7172
self._action_targets = {"app": self, "view": self.view}
7273

@@ -78,7 +79,7 @@ def run(
7879
cls, console: Console = None, screen: bool = True, driver: Type[Driver] = None
7980
):
8081
async def run_app() -> None:
81-
app = cls(console=console, screen=screen, driver=driver)
82+
app = cls(console=console, screen=screen, driver_class=driver)
8283
await app.process_messages()
8384

8485
asyncio.run(run_app())
@@ -95,11 +96,11 @@ async def process_messages(self) -> None:
9596
self.console.print_exception(show_locals=True)
9697

9798
async def _process_messages(self) -> None:
98-
log.debug("driver=%r", self.driver)
99+
log.debug("driver=%r", self.driver_class)
99100
loop = asyncio.get_event_loop()
100101

101102
loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt)
102-
driver = self.driver(self.console, self)
103+
driver = self._driver = self.driver_class(self.console, self)
103104

104105
active_app.set(self)
105106

@@ -116,32 +117,53 @@ async def _process_messages(self) -> None:
116117
await super().process_messages()
117118
finally:
118119
try:
119-
if self.children:
120-
121-
async def close_all() -> None:
122-
for child in self.children:
123-
await child.close_messages()
124-
await asyncio.gather(*(child.task for child in self.children))
125-
126-
try:
127-
await asyncio.wait_for(close_all(), timeout=5)
128-
except asyncio.TimeoutError as error:
129-
raise ShutdownError(
130-
"Timeout closing messages pump(s)"
131-
) from None
132-
133-
self.children.clear()
120+
driver.stop_application_mode()
134121
finally:
135-
try:
136-
driver.stop_application_mode()
137-
finally:
138-
loop.remove_signal_handler(signal.SIGINT)
122+
loop.remove_signal_handler(signal.SIGINT)
139123

140124
async def add(self, child: MessagePump) -> None:
141125
self.children.add(child)
142126
child.start_messages()
143127
await child.post_message(events.Created(sender=self))
144128

129+
async def remove(self, child: MessagePump) -> None:
130+
self.children.remove(child)
131+
132+
async def shutdown(self):
133+
driver = self._driver
134+
driver.disable_input()
135+
136+
async def shutdown_procedure() -> None:
137+
log.debug("1")
138+
await self.stop_messages()
139+
log.debug("2")
140+
await self.view.stop_messages()
141+
log.debug("3")
142+
log.debug("4")
143+
await self.remove(self.view)
144+
if self.children:
145+
log.debug("5")
146+
147+
async def close_all() -> None:
148+
for child in self.children:
149+
await child.close_messages(wait=False)
150+
await asyncio.gather(*(child.task for child in self.children))
151+
152+
try:
153+
await asyncio.wait_for(close_all(), timeout=5)
154+
log.debug("6")
155+
except asyncio.TimeoutError as error:
156+
raise ShutdownError("Timeout closing messages pump(s)") from None
157+
log.debug("7")
158+
159+
log.debug("8")
160+
await self.view.close_messages()
161+
log.debug("9")
162+
await self.close_messages()
163+
log.debug("10")
164+
165+
await asyncio.create_task(shutdown_procedure())
166+
145167
def refresh(self) -> None:
146168
console = self.console
147169
try:
@@ -150,7 +172,7 @@ def refresh(self) -> None:
150172
except Exception:
151173
log.exception("refresh failed")
152174

153-
async def on_event(self, event: events.Event, priority: int) -> None:
175+
async def on_event(self, event: events.Event) -> None:
154176
if isinstance(event, events.Key):
155177
key_action = self.KEYS.get(event.key, None)
156178
if key_action is not None:
@@ -160,7 +182,7 @@ async def on_event(self, event: events.Event, priority: int) -> None:
160182
if isinstance(event, events.InputEvent):
161183
await self.view.forward_input_event(event)
162184
else:
163-
await super().on_event(event, priority)
185+
await super().on_event(event)
164186

165187
async def on_idle(self, event: events.Idle) -> None:
166188
await self.view.post_message(event)
@@ -215,7 +237,7 @@ async def on_mouse_scroll_down(self, event: events.MouseScrollUp) -> None:
215237
await self.view.post_message(event)
216238

217239
async def action_quit(self) -> None:
218-
await self.close_messages()
240+
await self.shutdown()
219241

220242
async def action_bang(self) -> None:
221243
1 / 0

0 commit comments

Comments
 (0)