Problem z wyjściem z procesu - python asyncio

0

Witajcie

Od pewnego czasu mam problem z wyjściem z programu, który wykorzystuje bibliotekę asyncio. Pomysł jest prosty - chcę, aby program klienta został wyłączony po tym jak wyśle do serwera komendę exit. Napisałem już post na StackOverflow - https://stackoverflow.com/questions/52027965/cant-exit-asynchronous-process?noredirect=1#comment91008669_52027965, ale nie uzyskałem żadnej sensownej odpowiedzi do tej pory. Zastosowałem się do komentarze i poprawiłem kod, ale to sprawia, że przy wyłączeniu jednego procesu, kolejny proces, który włączam nie może pobrać żadnych danych z socketu od klienta. Czy ktoś wie w jaki sposób mógłbym wyjść z procesu?

Oto kod klas, które są najważniejsze do wyłapania błędu:

class FtpCommandsReceiver:
	def __init__(self, loop, sock):
		self.loop = loop
		self.sock = sock
		self.loop.create_task(self.recieve_data())
		self.commands_to_handle = {
								  		'exit': self.exit_handler
								  }

	async def recieve_data(self):
		while True:
			self.data_to_send = input('ftp> ')
			if self.data_to_send == '':
				continue
			await self.loop.sock_sendall(self.sock, self.data_to_send.encode())
			try:
				await self.commands_to_handle.get(self.data_to_send)()
			except TypeError:
				pass
			self.received_data = await self.loop.sock_recv(self.sock, 10000)
			print(self.received_data.decode())
			if not self.received_data:
				break
		print('Connection closed by the server')
		self.sock.close()

	async def exit_handler(self):
		self.loop.stop()

if __name__ == '__main__':
	loop = asyncio.get_event_loop()
	FTP_connection = FtpConnection(loop)
	task = loop.create_task(FTP_connection.connect())
	loop.run_forever()

Myślę, że problem może polegać na tym, że występuje tutaj ta instrukcja:

loop.run_forever()

Czy ktoś byłby w stanie mi pomóc? Będę bardzo wdzięczny, tym bardziej, że zupełnie nie wiem jak ten problem rozwiązać, a rzadko się zdarza, że mam taką pustkę w głowię.

0

Więcej informacji odnośnie tego jak kolejne odpalenie klienta się nie udaje byłoby pomocne. Jeżeli zwracany jest błąd zajętego portu, to przez to że poprzedni klient zostawił otwarty socket. Sprobuj zamknąć socket przed wywołaniem self.loop.stop():

    async def exit_handler(self):
        self.sock.close()
        self.loop.stop()
0

W załączniku znajdują się screeny z opisanym przeze mnie błędem.

Odwołując się do Twojej odpowiedzi to niestety nie mogę tak łatwo tego zrobić ze względu na kod operujący na danych, który wygląda tak:

async def recieve_data(self):
		while True:
			self.data_to_send = input('ftp> ')
			if self.data_to_send == '':
				continue
			await self.loop.sock_sendall(self.control_sock, self.data_to_send.encode())
			try:
				await self.commands_to_handle.get(self.data_to_send)()
			except TypeError:
				pass
			self.received_data = await self.loop.sock_recv(self.control_sock, 10000)
			print(self.received_data.decode())
			if not self.received_data:
				break
		print('Connection closed by the server')
		self.control_sock.close()

Najpierw ze słownika wywoływana jest określona metoda dla danej komendy jaką wywołał klient, następnie po jej wykonaniu przez socket odbierana jest komenda przekazana przez serwer i drukowana na ekranie. W przypadku gdyby serwer zamknął socket wtedy drukowane jest "Connection closed by the server" i zamknięty zostaje socket klienta. Dlatego w przypadku zamknięcia socketu w metodzie exit wówczas i na serwerze i na kliencie występuje taki oto wyjątek:

Task exception was never retrieved
future: <Task finished coro=<FtpCommandsReceiver.recieve_data() done, defined at FTPclient.py:51> exception=OSError(9, 'Bad file descriptor')>
Traceback (most recent call last):
  File "/usr/lib/python3.5/asyncio/tasks.py", line 239, in _step
    result = coro.send(None)
  File "FTPclient.py", line 61, in recieve_data
    self.received_data = await self.loop.sock_recv(self.control_sock, 10000)
  File "/usr/lib/python3.5/asyncio/futures.py", line 363, in __iter__
    return self.result()  # May raise too.
  File "/usr/lib/python3.5/asyncio/futures.py", line 274, in result
    raise self._exception
  File "/usr/lib/python3.5/asyncio/selector_events.py", line 334, in _sock_recv
    data = sock.recv(n)
OSError: [Errno 9] Bad file descriptor

Wydaję mi się, że po zamknięciu socketu na serwerze po prostu nie tworzą się nowe sockety przy połączeniu się klienta.

0

Przy tego typu problemach ciężko będzie pomóc bez sposobu na lokalną reprodukcję błędu. Jeżeli ten serwer docelowo ma trafić na Github, to sugerowałbym umieszczenie całego kodu na Githubie w aktualnym stanie, i dodanie kroków jak uruchomić serwer, uruchomić klienta, jakie komendy wydać itp, aby zreprodukować błąd.

0

Reprodukcja błędu:

1) W pierwszej konsoli: python3 <nazwa_pliku_z_kodem_serwera_ftp>.py, w drugiej konsoli: python3 <nazwa_pliku_z_kodem_serwera_ftp>.py
2) W drugiej konsoli należy się poprawnie zalogować, czyli (tekst w polu password może być dowolny):

Connected to the FTP server [127.0.1.1]
Name: anonymous
331 Password required for USER.
Password: 
230-
230- -------------------------------------------------------------------------
230- WELCOME!	This server is created by Kamil Szpakowski. You are logged in
230-            as anonymous.
230- -------------------------------------------------------------------------
ftp> 

3) W drugiej konsoli należy wpisać polecenie exit 2 razy:

Connected to the FTP server [127.0.1.1]

Name: anonymous
331 Password required for USER.
Password: 
230-
230- -------------------------------------------------------------------------
230- WELCOME!	This server is created by Kamil Szpakowski. You are logged in
230-            as anonymous.
230- -------------------------------------------------------------------------
ftp> exit
221 Goodbye.
ftp> exit

Connection closed by the server

4) Teraz trzeba uruchomić trzecią konsolę i spróbować się połączyć do serwera poprzez python3 <nazwa_pliku_z_kodem_serwera_ftp>.py Po wpisaniu 'anonymous' w polu 'Name: ' nic się nie dzieje i to jest właśnie błąd, który chciałbym wyeliminować.

Connected to the FTP server [127.0.1.1]

Name: anonymous


Kiedy wyślemy sygnał Ctrl+C wówczas pojawi się taki wyjątek przy programie klienta oczywiście:

Traceback (most recent call last):
  File "FTPclient.py", line 75, in <module>
    loop.run_forever()
  File "/usr/lib/python3.5/asyncio/base_events.py", line 345, in run_forever
    self._run_once()
  File "/usr/lib/python3.5/asyncio/base_events.py", line 1312, in _run_once
    handle._run()
  File "/usr/lib/python3.5/asyncio/events.py", line 125, in _run
    self._callback(*self._args)
  File "/usr/lib/python3.5/asyncio/tasks.py", line 239, in _step
    result = coro.send(None)
  File "FTPclient.py", line 35, in auth_to_the_server
    self.pass_required_msg = await self.loop.sock_recv(self.control_sock, 10000)
  File "/usr/lib/python3.5/asyncio/selector_events.py", line 318, in sock_recv
    self._sock_recv(fut, False, sock, n)
  File "/usr/lib/python3.5/asyncio/selector_events.py", line 334, in _sock_recv
    data = sock.recv(n)
KeyboardInterrupt
Task exception was never retrieved
future: <Task finished coro=<FtpAuthentication.auth_to_the_server() done, defined at FTPclient.py:32> exception=KeyboardInterrupt()>
Traceback (most recent call last):
  File "FTPclient.py", line 75, in <module>
    loop.run_forever()
  File "/usr/lib/python3.5/asyncio/base_events.py", line 345, in run_forever
    self._run_once()
  File "/usr/lib/python3.5/asyncio/base_events.py", line 1312, in _run_once
    handle._run()
  File "/usr/lib/python3.5/asyncio/events.py", line 125, in _run
    self._callback(*self._args)
  File "/usr/lib/python3.5/asyncio/tasks.py", line 239, in _step
    result = coro.send(None)
  File "FTPclient.py", line 35, in auth_to_the_server
    self.pass_required_msg = await self.loop.sock_recv(self.control_sock, 10000)
  File "/usr/lib/python3.5/asyncio/selector_events.py", line 318, in sock_recv
    self._sock_recv(fut, False, sock, n)
  File "/usr/lib/python3.5/asyncio/selector_events.py", line 334, in _sock_recv
    data = sock.recv(n)
KeyboardInterrupt

1

OK, przeanalizowałem troche ten kod.

  1. Nie używaj TABów do wcięć... https://www.python.org/dev/peps/pep-0008/#tabs-or-spaces

  2. Kilka newline'ów zwiększyłoby czytelność, zwłaszcza wokół ifów, wywołań najważniejszych metod, itp.

  3. Co do asyncio, widze tutaj mechanizm w stylu: funkcja blokująca -> loop.create_task(async1()) -> loop.create_task(async2()), czyli zagnieżdzone create_task(). Nigdy nie spotkałem się z takim sposobem wywoływania funkcji async. Jeżeli jesteś już w obrębie funkcji asynchronicznej, to wystarczy wywołać await async2(). Zamień wszystkie create_task(asyncX()) na await asyncX(), jeżeli ten create_task jest w obrębie funkcji asynchronicznej. Dotyczy to również wywoływania create_task() w __init__ obiektu który tworzony jest w funkcji asynchronicznej (np FtpCommandsReceiver() w kliencie). W takim przypadku można to zrobić tak (po usunięciu self.loop.create_task() z __init__()):

        FTP_commands_receiver = FtpCommandsReceiver(self.loop, self.control_sock)
        await FTP_commands_receiver.recieve_data()

Dobrą praktyką jest unikanie I/O w konstruktorze obiektu.

  1. Jeżeli pytamy klienta o hasło, należałoby to hasło odebrać. Podobnie, klient powinien wysłać hasło do serwera.
0
CaliforniaDreaming napisał(a):

OK, przeanalizowałem troche ten kod.

  1. Nie używaj TABów do wcięć... https://www.python.org/dev/peps/pep-0008/#tabs-or-spaces

  2. Kilka newline'ów zwiększyłoby czytelność, zwłaszcza wokół ifów, wywołań najważniejszych metod, itp.

  3. Co do asyncio, widze tutaj mechanizm w stylu: funkcja blokująca -> loop.create_task(async1()) -> loop.create_task(async2()), czyli zagnieżdzone create_task(). Nigdy nie spotkałem się z takim sposobem wywoływania funkcji async. Jeżeli jesteś już w obrębie funkcji asynchronicznej, to wystarczy wywołać await async2(). Zamień wszystkie create_task(asyncX()) na await asyncX(), jeżeli ten create_task jest w obrębie funkcji asynchronicznej. Dotyczy to również wywoływania create_task() w __init__ obiektu który tworzony jest w funkcji asynchronicznej (np FtpCommandsReceiver() w kliencie). W takim przypadku można to zrobić tak (po usunięciu self.loop.create_task() z __init__()):

        FTP_commands_receiver = FtpCommandsReceiver(self.loop, self.control_sock)
        await FTP_commands_receiver.recieve_data()

Dobrą praktyką jest unikanie I/O w konstruktorze obiektu.

  1. Jeżeli pytamy klienta o hasło, należałoby to hasło odebrać. Podobnie, klient powinien wysłać hasło do serwera.

Czyli metody AbstractEventLoop.create_task należy używać kiedy chcemy wywołać couroutine ze zwykłej metody? Jeśli wywołujemy metodę wewnątrz couroutine wtedy trzeba użyć await do wywołania?

1
Shizzer napisał(a):

Czyli metody AbstractEventLoop.create_task należy używać kiedy chcemy wywołać couroutine ze zwykłej metody? Jeśli wywołujemy metodę wewnątrz couroutine wtedy trzeba użyć await do wywołania?

Tak. Zamiast create_task() można też użyć asyncio.gather(): https://docs.python.org/3/library/asyncio-task.html#example-parallel-execution-of-tasks

1 użytkowników online, w tym zalogowanych: 0, gości: 1