11"""PythonHere app."""
2+
23# pylint: disable=wrong-import-order,wrong-import-position
34
45from launcher_here import try_startup_script
@@ -35,13 +36,25 @@ class PythonHereApp(App):
3536
3637 def __init__ (self ):
3738 super ().__init__ ()
38- self .server_task = None
39+ self .server_task : asyncio .Task | None = None
40+ self .app_task : asyncio .Task | None = None
41+ self .asyncio_loop : asyncio .AbstractEventLoop | None = None
3942 self .settings = None
43+
44+ # Created once a running asyncio loop exists.
45+ self .ssh_server_config_ready : asyncio .Event | None = None
46+ self .ssh_server_started : asyncio .Event | None = None
47+ self .ssh_server_connected : asyncio .Event | None = None
48+
49+ self .ssh_server_namespace = {}
50+ self .icon = "data/logo/logo-32.png"
51+
52+ def init_asyncio_state (self ):
53+ """Initialize asyncio-owned state after the event loop is running."""
54+ self .asyncio_loop = asyncio .get_running_loop ()
4055 self .ssh_server_config_ready = asyncio .Event ()
4156 self .ssh_server_started = asyncio .Event ()
4257 self .ssh_server_connected = asyncio .Event ()
43- self .ssh_server_namespace = {}
44- self .icon = "data/logo/logo-32.png"
4558
4659 @property
4760 def upload_dir (self ) -> str :
@@ -66,9 +79,7 @@ def build(self):
6679 """Initialize application UI."""
6780 super ().build ()
6881 install_exception_handler ()
69-
7082 self .settings = self .root .ids .settings
71-
7283 self .ssh_server_namespace .update (
7384 {
7485 "app" : self ,
@@ -81,47 +92,81 @@ def build(self):
8192 lambda _ : show_exception_popup (startup_script_exception ), 0
8293 )
8394
84- def run_app (self ):
85- """Run application and SSH server tasks."""
86- self .ssh_server_started = asyncio .Event ()
87- self .server_task = asyncio .ensure_future (run_ssh_server (self ))
88- return asyncio .gather (self .async_run_app (), self .server_task )
95+ async def run_app (self ):
96+ """Run application and SSH server until either main task exits."""
97+ self .init_asyncio_state ()
98+
99+ self .server_task = asyncio .create_task (run_ssh_server (self ))
100+ self .app_task = asyncio .create_task (self .async_run_app ())
101+
102+ tasks = (self .server_task , self .app_task )
103+
104+ try :
105+ done , pending = await asyncio .wait (
106+ tasks ,
107+ return_when = asyncio .FIRST_COMPLETED ,
108+ )
109+
110+ for task in pending :
111+ task .cancel ()
112+
113+ if pending :
114+ await asyncio .gather (* pending , return_exceptions = True )
115+
116+ for task in done :
117+ if task .cancelled ():
118+ continue
119+ exc = task .exception ()
120+ if exc is not None :
121+ raise exc
122+ finally :
123+ await self .cancel_app_tasks ()
89124
90125 async def async_run_app (self ):
91126 """Run app asynchronously."""
92127 try :
93- await self .async_run (async_lib = "asyncio" )
128+ await self .async_run ()
94129 Logger .info ("PythonHere: async run completed" )
95130 except asyncio .CancelledError :
96131 Logger .info ("PythonHere: app main task canceled" )
132+ raise
97133 except Exception as exc :
98134 Logger .exception (exc )
135+ raise
136+ finally :
137+ if self .get_running_app ():
138+ self .stop ()
99139
100- if self .server_task :
101- self .server_task .cancel ()
102-
103- if self .get_running_app ():
104- self .stop ()
105-
106- await self .cancel_asyncio_tasks ()
107-
108- async def cancel_asyncio_tasks (self ):
109- """Cancel all asyncio tasks."""
140+ async def cancel_app_tasks (self ):
141+ """Cancel tasks owned by this app."""
110142 tasks = [
111- task for task in asyncio .all_tasks () if task is not asyncio .current_task ()
143+ task
144+ for task in (self .server_task , self .app_task )
145+ if task is not None
146+ and task is not asyncio .current_task ()
147+ and not task .done ()
112148 ]
149+
150+ for task in tasks :
151+ task .cancel ()
152+
113153 if tasks :
114- for task in tasks :
115- task .cancel ()
116- await asyncio .wait (tasks , timeout = 1 )
154+ await asyncio .gather (* tasks , return_exceptions = True )
117155
118156 def update_server_config_status (self ):
119157 """Check and update value of the `ssh_server_config_ready`, update screen."""
120158
121159 def update ():
122160 if all (self .get_pythonhere_config ().values ()):
123- self .ssh_server_config_ready .set ()
124- screen .update ()
161+ if (
162+ self .asyncio_loop is not None
163+ and self .ssh_server_config_ready is not None
164+ ):
165+ self .asyncio_loop .call_soon_threadsafe (
166+ self .ssh_server_config_ready .set
167+ )
168+
169+ Clock .schedule_once (lambda _ : screen .update (), 0 )
125170
126171 screen = self .root .ids .here_screen_manager
127172 screen .current = ServerState .starting_server
@@ -151,8 +196,14 @@ def on_pause(self):
151196 def on_ssh_connection_made (self ):
152197 """New authenticated SSH client connected handler."""
153198 Logger .info ("PythonHere: new SSH client connected" )
199+
200+ if self .ssh_server_connected is None :
201+ Logger .warning ("PythonHere: SSH connected before asyncio state was ready" )
202+ return
203+
154204 if not self .ssh_server_connected .is_set ():
155205 self .ssh_server_connected .set ()
206+
156207 Logger .info ("PythonHere: reset window environment" )
157208 self .ssh_server_namespace ["root" ] = reset_window_environment ()
158209 self .chdir (self .upload_dir )
@@ -164,7 +215,11 @@ def chdir(self, path: str):
164215 sys .path .insert (0 , path )
165216
166217
218+ async def main ():
219+ """Run PythonHere."""
220+ app = PythonHereApp ()
221+ await app .run_app ()
222+
223+
167224if __name__ == "__main__" :
168- loop = asyncio .get_event_loop ()
169- loop .run_until_complete (PythonHereApp ().run_app ())
170- loop .close ()
225+ asyncio .run (main ())
0 commit comments