Problem z wyjściem z procesu - python asyncio

Problem z wyjściem z procesu - python asyncio
Shizzer
  • Rejestracja:prawie 8 lat
  • Ostatnio:4 miesiące
  • Postów:231
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:

Kopiuj
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:

Kopiuj
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ę.


CD
  • Rejestracja:ponad 6 lat
  • Ostatnio:około 2 lata
  • Postów:20
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():

Kopiuj
    async def exit_handler(self):
        self.sock.close()
        self.loop.stop()
Shizzer
  • Rejestracja:prawie 8 lat
  • Ostatnio:4 miesiące
  • Postów:231
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:

Kopiuj
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:

Kopiuj
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.


CD
  • Rejestracja:ponad 6 lat
  • Ostatnio:około 2 lata
  • Postów:20
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.

CD
No to brakuje jeszcze procedury reprodukcji błędu ;)
Shizzer
  • Rejestracja:prawie 8 lat
  • Ostatnio:4 miesiące
  • Postów:231
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):

Kopiuj
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:

Kopiuj
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ć.

Kopiuj
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:

Kopiuj
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


CD
A czy serwer był testowany pod kątem wielu połączeń od klientów?
Shizzer
Tak i nie było problemów, natomiast wygląda na to, że wszystkie połączenia są przyjmowane na jednym socketcie
CD
  • Rejestracja:ponad 6 lat
  • Ostatnio:około 2 lata
  • Postów:20
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__()):

Kopiuj
        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.
Shizzer
  • Rejestracja:prawie 8 lat
  • Ostatnio:4 miesiące
  • Postów:231
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__()):

Kopiuj
        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?


CD
  • Rejestracja:ponad 6 lat
  • Ostatnio:około 2 lata
  • Postów:20
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

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)