Podgląd i zmiana wartości zmiennych in runtime – possible?!

Cześć,
jak pisałem we wcześniejszym wpisie, różne zdarzenia życiowe spowodowały że nagle mam znacznie więcej czasu, więc dziś poopowiadam Wam o ciekawym libie Python’owym – manhole. Z angielskiego manhole znaczy.. Właz. Kanalizacyjny. Taki jak mijacie na ulicach. A co on robi w Pythonie? Ano zgodnie z tytułem wpisu – pozwala wejść do programu w sposób interaktywny trochę tak „z boku”.

Ale powoli, napiszmy kawałek kodu który będzie agregował jakieś dane w pamięci. Niech ten program w odpowiedzi na wysyłanie do niego komunikatów wykonuje operacje które znajdują się w tym komunikacie. Uprośćmy to maksymalnie – komunikaty będą kazały wykonywać jedną z podstawowych działań matematycznych z aktualną wartością w pamięci a wartością wysłaną w owym komunikacie. Brzmi zagmatwanie? Nieee, jest turbo proste.

Stwórzmy 3 pliki w takiej hierarchii:

1
2
3
4
5
data_store
  __init__.py
subscriber
  __init__.py
main.py

I plik w folderze data_store niech ma taką zawartość:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import inspect
myself = lambda: inspect.stack()[1][3]


class DataStore():
    def __init__(self):
        self.current_value = 0
        self.history = []

    def __add__(self, other):
        self.current_value = self.current_value + other
        self.history.append([
            other,
            str(myself())
        ])

    def __mul__(self, other):
        self.current_value = self.current_value + other
        self.history.append([
            other,
            str(myself())
        ])

    def __sub__(self, other):
        self.current_value = self.current_value - other
        self.history.append([
            other,
            str(myself())
        ])

    def __div__(self, other):
        self.current_value = self.current_value / other
        self.history.append([
            other,
            str(myself())
        ])

    def __str__(self):
        return str(self.current_value)


data_store = DataStore()

Nic skomplikowanego – klasa definiuje zachowania operatorów dodawania, odejmowania, mnożenia i dzielenia jako operacje na jednej z zmiennych tej klasy i zapisuje te zmiany jako historię.

Teraz plik w folderze subscriber:

1
2
3
4
5
6
7
8
9
10
import redis


def main(worker):
    redis_conn = redis.Redis()
    subscriber = redis_conn.pubsub()
    subscriber.subscribe('test')

    for item in subscriber.listen():
        worker(item['data'])

Co tu robimy? Prawie nic. Na każdą wiadomość która przyjdzie na kanał redisowy o nazwe „test” reagujemy wykonując przekazaną w parametrze funkcję. Banalne, nie?

I na koniec plik main.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from subscriber import main
from datastore import data_store
import operator
from json import loads


def worker(data):
    try:
        data = loads(data)
        print data
        op_func = getattr(operator, data['operation'])
        op_func(data_store, data['value'])
    except:
        pass


if __name__ == '__main__':
    main(worker)

Jako funkcję wejściową wykonujemy funkcję ze subscriber’a i przekazując mu jako argument naszą funkcję. Jaki będzie efekt? Bardzo prosty:

Jeśli na kanał wrzucimy 3 razy coś takiego:

1
redis-cli publish test '{"operation": "add", "value":6}'

To funkcja „worker” pobierze metodę dodającą i wykona na aktualnej wartości przechowywanej w data storze tę metodę. Co przy pierwszym wysłaniu spowoduje dodanie do 0 liczby 6. Drugie – do liczby 6 kolejną 6. I trzecie – do liczby 12(sumy) – kolejną 6. Co powoduje przechowanie w pamięci liczby 18 i 3 elementowej listy z historią tych operacji.

I teraz, na białym koniu, cały na biało – wjeżdża manhole.

Bo co jeśli w kodzie jest np. błąd i wartość przechowywana w pamięci jest zepsuta? Oczywiście można logować każdą zmianę i w myśl event source’ingu – odtworzyć stan i tak go skorygować dodatkowymi komunikatami by był poprawny, ale raz że to dość pracochłonne zajęcie a dwa wymusza wyczyszczenie pamięci programu co może wiązać się różnymi konsekwencjami. Np. straceniem tych danych które są poprawne lub przerwę w działaniu usługi.

Zmodyfikujmy więc nasz kod troszkę:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from subscriber import main
from datastore import data_store
import operator
from json import loads
import manhole


def worker(data):
    if data == 'enable_manhole':
        manhole.install(locals={'data_store':data_store})
    else:
        try:
            data = loads(data)
            print data
            op_func = getattr(operator, data['operation'])
            op_func(data_store, data['value'])
        except:
            pass


if __name__ == '__main__':
    main(worker)

Dodaliśmy tu obsługę „zainstalowania” naszego włazu programistycznego. Wiec sprawdźmy to! Wyślijmy:

1
redis-cli publish test 'enable_manhole'

co spowoduje wypisanie na standardowe wyjście naszego programu:

1
2
3
Manhole[17744:1524079049.5979]: Patched <built-in function fork> and <built-in function fork>.
Manhole[17744:1524079049.5988]: Manhole UDS path: /tmp/manhole-17744
Manhole[17744:1524079049.5988]: Waiting for new connection (in pid:17744) ...

Co znaczy że manhole się zainstalował w osobnym wątku i udostępnia dostęp do programu (a dokładniej do zmiennej data_store co jest zdefiniowane parametrem locals) jako socket unixowy pod ścieżką /tmp/manhole-17744.

Połączmy się!

1
 sudo nc -U /tmp/manhole-17744

Co spowoduje info o przyjęciu połączenia:

1
2
Manhole[17744:1524079333.6725]: Started ManholeConnectionThread thread. Checking credentials ...
Manhole[17744:1524079333.6726]: Accepted connection on fd:5 from PID:18093 UID:0 GID:0

a my mamy dostęp do programu w formie interaktywnej konsoli Pythona:

możemy też edytować zmienne:

Wydaje mi się że łatwo zauważyć potencjał tej biblioteki.

Dzięki za wizytę,
Mateusz Mazurek
Podziel się na:
    Facebook email PDF Wykop Twitter

One thought on “Podgląd i zmiana wartości zmiennych in runtime – possible?!

  1. Ciekawy artykuł. Zdaje się, że koniecznie muszę nadrobić swoje zaległości jeśli chodzi o redisa, bo kiedy w przeszłości miałem okazję używać tej bazy, to nigdy nie bawiłem się w niej kanałami, a wyglądają one na zdecydowanie przydatną funkcjonalność. ;)

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

This site uses Akismet to reduce spam. Learn how your comment data is processed.