Quit netconf-cli if `ssh` crashes

Before this, if the our ssh process crashed, we wouldn't know until we
pressed issued a command. This patch adds a waiter/watcher thread which
issues "enter" when the ssh process crashes and that escapes
replxx::Replxx::input. Then cli gracefully exits.

Issue: https://tree.taiga.io/project/jktjkt-netconf-cli/issue/195
Change-Id: I931987626d2dbba0e488987dff15cca22481edfb
diff --git a/src/cli.cpp b/src/cli.cpp
index 4f4328a..3118d94 100644
--- a/src/cli.cpp
+++ b/src/cli.cpp
@@ -5,6 +5,7 @@
  * Written by Václav Kubernát <kubervac@fit.cvut.cz>
  *
 */
+#include <atomic>
 #include <docopt.h>
 #include <iostream>
 #include <optional>
@@ -63,6 +64,16 @@
 #include "cli-netconf.hpp"
 #include "netconf_access.hpp"
 #define PROGRAM_NAME "netconf-access"
+// FIXME: this should be replaced by C++20 std::jthread at some point
+struct PoorMansJThread {
+    ~PoorMansJThread()
+    {
+        if (thread.joinable()) {
+            thread.join();
+        }
+    }
+    std::thread thread;
+};
 #else
 #error "Unknown CLI backend"
 #endif
@@ -78,6 +89,10 @@
                                true);
     WritableOps writableOps = WritableOps::No;
 
+    using replxx::Replxx;
+    Replxx lineEditor;
+    std::atomic<int> backendReturnCode = 0;
+
 #if defined(SYSREPO_CLI)
     auto datastoreType = Datastore::Running;
     if (const auto& ds = args["-d"]) {
@@ -143,10 +158,21 @@
     }
 
     SshProcess process;
+    PoorMansJThread processWatcher;
     std::shared_ptr<NetconfAccess> datastore;
 
     try {
         process = sshProcess(args.at("<host>").asString(), args.at("-p").asString());
+        processWatcher.thread = std::thread([&process, &lineEditor, &backendReturnCode] () {
+            process.process.wait();
+            backendReturnCode = process.process.exit_code();
+            // CTRL-U clears from the cursor to the start of the line
+            // CTRL-K clears from the cursor to the end of the line
+            // CTRL-D send EOF
+            lineEditor.emulate_key_press(replxx::Replxx::KEY::control('U'));
+            lineEditor.emulate_key_press(replxx::Replxx::KEY::control('K'));
+            lineEditor.emulate_key_press(replxx::Replxx::KEY::control('D'));
+        });
         datastore = std::make_shared<NetconfAccess>(process.std_out.native_source(), process.std_in.native_sink());
     } catch (std::runtime_error& ex) {
         std::cerr << "SSH connection failed: " << ex.what() << "\n";
@@ -170,10 +196,6 @@
     auto dataQuery = std::make_shared<DataQuery>(*datastore);
     Parser parser(datastore->schema(), writableOps, dataQuery);
 
-    using replxx::Replxx;
-
-    Replxx lineEditor;
-
     lineEditor.bind_key(Replxx::KEY::meta(Replxx::KEY::BACKSPACE), [&lineEditor](const auto& code) {
         return lineEditor.invoke(Replxx::ACTION::KILL_TO_BEGINING_OF_WORD, code);
     });
@@ -204,7 +226,7 @@
         lineEditor.history_load(historyFile.value());
     }
 
-    while (true) {
+    while (backendReturnCode == 0) {
         auto line = lineEditor.input(parser.currentNode() + "> ");
         if (!line) {
             // If user pressed CTRL-C to abort the line, errno gets set to EAGAIN.
@@ -242,5 +264,5 @@
         lineEditor.history_save(historyFile.value());
     }
 
-    return 0;
+    return backendReturnCode;
 }