eclipse / paho.mqtt.python

paho.mqtt.python
Other
2.19k stars 721 forks source link

Proper way to catch CTRL-C with mqtt client where 'will_set' gets respected #700

Closed mmattel closed 9 months ago

mmattel commented 1 year ago

I am desperately looking for a hint how to properly implement a keyboard interrupt (ctrl-c) in a way that on_connect and last_will will get respected. I searched a lot but have not found any hint.

At the moment using the code below, whenever I press ctrl-c, the program exits correctly and will_set is respected - but you get a ctrl-c message printed on the console.

def on_connect(client, userdata, flags, rc):
    client.publish("test/status", payload="Online", qos=0, retain=True)

mqttclient = mqtt.Client(client_id=mqtt_client_id, clean_session=True)
mqttclient.on_connect = on_connect
mqttclient.username_pw_set(mqtt_username, mqtt_password)
mqttclient.will_set("test/status", payload="Offline", qos=0, retain=True)

mqttclient.connect_async(mqtt_server, port=mqtt_port, keepalive=70)
mqttclient.loop_start()

while True:
    do_stuff

mqttclient.disconnect()
mqttclient.loop_stop()

Adapting the code by adding a signal handler or a try/except in the while loop, pressing ctrl-c does not print the exit message, but will_set gets not respected.

Tips and hints how to solve this are welcomed.

pdcastro commented 1 year ago

I have come across a similar scenario and I think I have figured it out. My SIGINT handler was gracefully calling mqttclient.disconnect() — maybe yours was too? — and I observed that the Last Will message was not being published. It turns out that this is the expected behaviour. The MQTT protocol makes a distinction between graceful and ungraceful disconnects (ref) and defines that the Last Will only applies to ungraceful disconnects, for example a dropped network connection, device power loss, or the client process being suddenly killed by the OS (e.g. kill -9 $PID on a Linux prompt). When you say that a CTRL-C message gets printed to the console, I imagine that this means that your shell/OS forcibly kills the process, simulating an ungraceful disconnect.

I think it makes sense for a SIGINT handler to gracefully terminate the client including a disconnect() call, but in this case the Last Will message will not be automatically published, by design. What I have done is to explicitly publish the same message as the Last Will through a regular publish() call, just before the disconnect() call in the signal handler. Et voilà!

mmattel commented 1 year ago

Thanks for the explanation, this guided me into the right direction, but it was not complete. When initiating a publish and disconnect right afterwards, the message will not be sent as disconnect is faster than publishing. You need to wait until it was published, then you can disconnect. Here is the code I am using now successfully.

def graceful_shutdown():
    print()
    # the will_set is not sent on graceful shutdown by design
    # we need to wait until the message has been sent, else it will not appear in the broker
    publish_result = mqttclient.publish(mqtt_state_topic, payload = "offline", qos = mqtt_qos, retain = True)
    publish_result.wait_for_publish() 
    mqttclient.disconnect()
    mqttclient.loop_stop()
    sys.exit()

# catch ctrl-c
def signal_handler(signum, frame):
    graceful_shutdown()

signal.signal(signal.SIGINT, signal_handler)
MattBrittan commented 9 months ago

I'm going to close this because it looks like you arrived at a pretty decent answer yourself! Please feel free to reopen if more info is needed (we are trying to get on top of the issues!).

Note: This is part of an exercise to clean up old issues so that the project can move forwards. Due to the number of issues being worked through mistakes will be made; please feel free to reopen this issue (or comment) if you believe it's been closed in error.