SHELL: some configurationing, serial port supports and fixes
[far2l.git] / NetRocks / src / Protocol / SHELL / WayToShell.cpp
blob9cf1cfe9d4e67d0e26cfc4de4f66fa4e77add6cd
1 #include "WayToShell.h"
2 #include "Parse.h"
3 #include <utils.h>
4 #include <MakePTYAndFork.h>
5 #include <time.h>
6 #include <termios.h> // Для работы с терминальными атрибутами
7 #include <signal.h>
8 #include <os_call.hpp>
9 #include <Environment.h>
10 #include <MatchWildcard.hpp>
11 #include "../../Erroring.h"
13 // use 'export NETROCKS_VERBOSE=1' or '..=2' to see protocol dumps from DebugStr
14 static void DebugStr(const char *info, const char *str, size_t len)
16 std::string tmp;
17 for (size_t i = 0; i <= len; ++i) {
18 if (i == len) {
19 ; // nothing
20 } else if ((unsigned char)str[i] < 32) {
21 tmp+= StrPrintf("{%02x}", (unsigned char)str[i]);
22 if (str[i] == '\r' && i + 1 < len && str[i + 1] == '\n') {
23 continue;
25 } else {
26 tmp+= str[i];
28 if (!tmp.empty() && (i == len || str[i] == '\r' || str[i] == '\n')) {
29 std::cerr << info << ": " << tmp << std::endl;
30 tmp.clear();
35 static void DebugStr(const char *info, const std::string &str)
37 DebugStr(info, str.c_str(), str.size());
40 static void ForkSafePrint(const char *str)
42 if (write(STDOUT_FILENO, str, strlen(str)) <= 0) {
43 perror("write");
47 WayToShell::WayToShell(int fd_ipc_recv, const WayToShellConfig &cfg, const StringConfig &protocol_options)
48 : _fd_ipc_recv(fd_ipc_recv)
50 if (!cfg.command.empty()) {
51 LaunchCommand(cfg, protocol_options);
52 } else if (!cfg.serial.empty()) {
53 OpenSerialPort(cfg, protocol_options);
54 } else {
55 throw ProtocolError("Command or port not specified");
59 // That long echo after exit makes things work smooter, dunno why.
60 // And it really needs to be rather long
61 static const char s_exit_cmd[] = "\nexit\necho =================================\n";
63 WayToShell::~WayToShell()
65 try {
66 while (FinalRead(100)) {
67 usleep(1000);
69 if (write(_master_fd, s_exit_cmd, sizeof(s_exit_cmd) - 1) == sizeof(s_exit_cmd) - 1) {
70 FinalRead(1000);
71 fprintf(stderr, "~WayToShell: exit delivered\n");
72 } else {
73 perror("~WayToShell: write exit");
76 } catch (...) {
77 fprintf(stderr, "~WayToShell: exit exception\n");
80 CheckedCloseFD(_master_fd);
81 CheckedCloseFDPair(_stderr_pipe);
82 if (_pid != (pid_t)-1) {
83 // Проверяем, завершен ли дочерний процесс
84 int status;
85 if (waitpid(_pid, &status, WNOHANG) == 0) {
86 // Если нет, отправляем сигнал для завершения
87 kill(_pid, SIGTERM);
88 waitpid(_pid, &status, 0); // Ожидаем завершения
90 std::cerr << "Child exited with status " << status << '\n';
94 bool WayToShell::FinalRead(int tmout)
96 struct pollfd pfd{};
97 pfd.fd = _master_fd;
98 pfd.events = POLLIN;
99 const int r = os_call_int(poll, &pfd, (nfds_t)1, tmout);
100 return (r > 0 && RecvPolledFD(pfd, STDOUT));
103 void WayToShell::LaunchCommand(const WayToShellConfig &cfg, const StringConfig &protocol_options)
105 std::string command = cfg.command;
106 for (unsigned i = cfg.options.size(); i--;) { // reversed order for proper replacement
107 Substitute(command, StrPrintf("$OPT%u", i).c_str(), cfg.OptionValue(i, protocol_options));
109 if (g_netrocks_verbosity > 0) {
110 fprintf(stderr, "WayToShell::LaunchCommand: '%s'\n", command.c_str());
113 if (pipe(_stderr_pipe) < 0) {
114 throw ProtocolError("pipe error", errno);
117 struct Argv : std::vector<char *>
119 ~Argv()
121 for (auto &arg : *this) {
122 free(arg);
125 } argv;
127 Environment::ExplodeCommandLine ecl(command);
128 for (const auto &arg : ecl) {
129 argv.emplace_back(strdup(arg.c_str()));
131 argv.emplace_back(nullptr);
133 _pid = MakePTYAndFork(_master_fd);
135 if (_pid == (pid_t)-1) {
136 throw ProtocolError("PTY error", errno);
139 if (_pid == 0) {
140 CheckedCloseFD(_stderr_pipe[0]); // close read end
141 dup2(_stderr_pipe[1], STDERR_FILENO); // redirect stderr to the pipe
142 CheckedCloseFD(_stderr_pipe[1]); // close write end, as it's now duplicated
144 setenv("LANG", "C", 1);
145 setenv("TERM", "xterm-mono", 1);
146 unsetenv("COLORFGBG");
147 // Child process
148 // Получаем текущие атрибуты терминала
149 struct termios tios{};
150 if (tcgetattr(STDIN_FILENO, &tios) == -1) {
151 ForkSafePrint("tcgetattr failed\n");
152 _exit(-1);
155 // disable echo, CR/LF translation and other nasty things
156 cfmakeraw(&tios);
158 // Применяем новые атрибуты
159 if (tcsetattr(STDIN_FILENO, TCSANOW, &tios) == -1) {
160 ForkSafePrint("tcsetattr failed\n");
161 _exit(-1);
163 // ssh should terminate if terminal closed
164 signal(SIGINT, SIG_DFL);
165 signal(SIGHUP, SIG_DFL);
166 signal(SIGPIPE, SIG_DFL);
168 execvp(*argv.data(), argv.data());
169 ForkSafePrint("execlp failed\n");
170 _exit(-1);
173 // Parent process
174 CheckedCloseFD(_stderr_pipe[1]); // close write end
176 // Now you can read from _stderr_pipe[0] to get the stderr output
177 // and from _master_fd to get the stdout output
179 MakeFDNonBlocking(_master_fd);
180 MakeFDNonBlocking(_stderr_pipe[0]);
183 void WayToShell::OpenSerialPort(const WayToShellConfig &cfg, const StringConfig &protocol_options)
185 const auto &opt_baudrate = cfg.OptionValue(0, protocol_options);
186 const auto &opt_flowctrl = cfg.OptionValue(1, protocol_options);
187 const auto &opt_databits = cfg.OptionValue(2, protocol_options);
188 const auto &opt_stopbits = cfg.OptionValue(3, protocol_options);
190 speed_t baudrate;
191 switch (atoi(opt_baudrate.c_str())) {
192 case 50: baudrate = B50; break;
193 case 150: baudrate = B150; break;
194 case 300: baudrate = B300; break;
195 case 600: baudrate = B600; break;
196 case 1200: baudrate = B1200; break;
197 case 2400: baudrate = B2400; break;
198 case 4800: baudrate = B4800; break;
199 case 9600: baudrate = B9600; break;
200 case 19200: baudrate = B19200; break;
201 case 38400: baudrate = B38400; break;
202 case 57600: baudrate = B57600; break;
203 case 115200: baudrate = B115200; break;
204 case 230400: baudrate = B230400; break;
205 case 460800: baudrate = B460800; break;
206 case 500000: baudrate = B500000; break;
207 case 576000: baudrate = B576000; break;
208 case 921600: baudrate = B921600; break;
209 case 1000000: baudrate = B1000000; break;
210 case 1152000: baudrate = B1152000; break;
211 case 1500000: baudrate = B1500000; break;
212 case 2000000: baudrate = B2000000; break;
213 case 2500000: baudrate = B2500000; break;
214 case 3000000: baudrate = B3000000; break;
215 case 3500000: baudrate = B3500000; break;
216 case 4000000: baudrate = B4000000; break;
217 default:
218 throw ProtocolError("Bad baudrate", opt_baudrate.c_str());
221 _master_fd = open(cfg.serial.c_str(), O_RDWR);
222 if (_master_fd == -1) {
223 throw ProtocolError("open port error", cfg.serial.c_str(), errno);
226 struct termios tios{};
227 if (tcgetattr(_master_fd, &tios) != 0) {
228 const int err = errno;
229 close(_master_fd);
230 throw ProtocolError("tcgetattr error", cfg.serial.c_str(), err);
233 tios.c_cflag&= ~PARENB;
234 tios.c_cflag&= ~CSTOPB;
235 tios.c_cflag&= ~CSIZE;
237 tios.c_cflag&= ~CRTSCTS;
238 tios.c_cflag|= CREAD | CLOCAL;
240 tios.c_lflag&= ~ISIG;
241 tios.c_lflag&= ~ICANON;
242 tios.c_lflag&= ~ECHO;
243 tios.c_lflag&= ~ECHOE;
244 tios.c_lflag&= ~ECHONL;
246 tios.c_iflag&= ~(IXON | IXOFF | IXANY);
247 tios.c_iflag&= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL);
249 tios.c_oflag &= ~OPOST;
250 tios.c_oflag &= ~ONLCR;
252 tios.c_cc[VTIME] = 0;
253 tios.c_cc[VMIN] = 1;
255 switch (atoi(opt_databits.c_str())) {
256 case 5: tios.c_cflag|= CS5; break;
257 case 6: tios.c_cflag|= CS6; break;
258 case 7: tios.c_cflag|= CS7; break;
259 case 8: tios.c_cflag|= CS8; break;
260 default:
261 close(_master_fd);
262 throw ProtocolError("wrong databits", opt_databits.c_str());
265 if (opt_flowctrl == "Xon/Xoff") {
266 tios.c_iflag|= (IXON | IXOFF);
267 } else if (opt_flowctrl == "RTS/CTS") {
268 tios.c_cflag|= CRTSCTS;
269 } else if (opt_flowctrl != "None") {
270 close(_master_fd);
271 throw ProtocolError("wrong flowctrl", opt_flowctrl.c_str());
274 if (opt_stopbits == "2") {
275 tios.c_cflag|= CSTOPB;
276 } else if (opt_stopbits != "1") {
277 close(_master_fd);
278 throw ProtocolError("wrong stopbits", opt_stopbits.c_str());
281 // cfmakeraw(&tios);
282 cfsetispeed(&tios, baudrate);
283 cfsetospeed(&tios, baudrate);
285 if (tcsetattr(_master_fd, TCSANOW, &tios) != 0) {
286 const int err = errno;
287 close(_master_fd);
288 throw ProtocolError("tcsetattr error", cfg.serial.c_str(), err);
291 if (pipe(_stderr_pipe) < 0) { // create dummy pipe, just for genericity
292 const int err = errno;
293 close(_master_fd);
294 throw ProtocolError("pipe error", err);
297 MakeFDNonBlocking(_master_fd);
298 MakeFDNonBlocking(_stderr_pipe[0]);
301 void WayToShell::GetDescriptors(int &fdinout, int &fderr)
303 fdinout = (_master_fd != -1) ? dup(_master_fd) : -1;
304 fderr = (_stderr_pipe[0] != -1) ? dup(_stderr_pipe[0]) : -1;
307 void WayToShell::ThrowIfAppExited()
309 if (_pid != (pid_t)-1) {
310 // usleep(10000);
311 if (waitpid(_pid, &_pid_status, WNOHANG) == _pid) {
312 _pid = (pid_t)-1;
313 throw ProtocolError("client app exited", _pid_status);
318 bool WayToShell::RecvPolledFD(struct pollfd &fd, enum OutputType output_type)
320 if (!(fd.revents & POLLIN)) {
321 return false;
324 std::vector<char> &data = (output_type == STDERR) ? _stderr_data : _stdout_data;
325 const size_t ofs = data.size();
326 const size_t piece = 4096;
327 data.resize(data.size() + piece);
328 ssize_t n = os_call_ssize(read, fd.fd, (void *)(data.data() + ofs), piece);
329 if (n <= 0) {
330 fprintf(stderr, " !!! error reading pty n=%ld err=%d\n", n, errno);
331 data.resize(ofs);
332 throw ProtocolError("error reading pty", (output_type == STDERR) ? "stderr" : "stdout", errno);
334 data.resize(ofs + n);
336 // if (g_netrocks_verbosity > 0) {
337 // DebugStr((output_type == STDERR) ? "STDERR" : "STDOUT", data.data() + ofs, n);
338 // }
340 return true;
343 // if data is non-NULL then send that data, optionally recving incoming stuff
344 // if data is NULL then recv at least some incoming stuff
345 unsigned WayToShell::Xfer(const char *data, size_t len)
347 unsigned out = 0;
348 while ((!data && out == 0) || len) {
349 struct pollfd fds[3]{};
350 fds[0].fd = _master_fd;
351 fds[0].events = (data && len) ? POLLIN | POLLOUT : POLLIN;
352 fds[1].fd = _stderr_pipe[0];
353 fds[1].events = POLLIN;
354 fds[2].fd = _fd_ipc_recv;
355 fds[2].events = 0; // this poll'ed only for errors
357 const int r = os_call_int(poll, &fds[0], (nfds_t)3, 10000);
358 if (r < 0) {
359 throw ProtocolError("error polling pty", errno);
362 if (r == 0) {
363 ThrowIfAppExited();
365 } else {
366 if (RecvPolledFD(fds[0], STDOUT)) {
367 out|= STDOUT;
369 if (RecvPolledFD(fds[1], STDERR)) {
370 out|= STDERR;
372 if (data && len && (fds[0].revents & POLLOUT) != 0) {
373 const ssize_t wr = write(_master_fd, data, len);
374 if (wr >= 0) {
375 len-= wr;
376 if (len != 0) {
377 data+= wr;
380 } else if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
381 throw ProtocolError("pty write error", errno);
384 if ( (fds[0].revents & (POLLERR | POLLHUP)) != 0
385 || (fds[1].revents & (POLLERR | POLLHUP)) != 0) {
386 throw ProtocolError("pty disrupted", errno);
388 if ((fds[2].revents & (POLLERR | POLLHUP)) != 0) {
389 throw ProtocolError("ipc disrupted", errno);
393 return out;
396 void WayToShell::RecvSomeStdout()
398 try {
399 while ((Xfer() & STDOUT) == 0) {
402 } catch (std::exception &e) {
403 if (_stderr_data.empty()) {
404 throw;
406 std::string stderr_str(_stderr_data.data(), _stderr_data.size());
407 throw ProtocolError(e.what(), stderr_str.c_str());
411 static ssize_t DispatchExpectedReply(std::vector<std::string> &lines, std::vector<char> &incoming,
412 const std::vector<const char *> &expected_replies)
414 // fetch incoming data and pack it into lines only until found match with expected reply
415 for (size_t left = 0, right = 0;;) {
416 if (right == incoming.size() || incoming[right] == '\r' || incoming[right] == '\n' || incoming[right] == 0) {
417 if (lines.empty() || lines.back().back() == '\n') {
418 lines.emplace_back(incoming.data() + left, right - left);
419 } else {
420 lines.back().append(incoming.data() + left, right - left);
422 const bool append_lf = (right != incoming.size());
423 if (right + 1 < incoming.size() && incoming[right] == '\r' && incoming[right + 1] == '\n') {
424 right+= 2;
425 } else if (right < incoming.size()) {
426 right++;
428 left = right;
429 if (!lines.back().empty()) {
430 if (append_lf) {
431 lines.back()+= '\n';
433 if (g_netrocks_verbosity > 1) {
434 std::cerr << "Line '" << lines.back() << "' " << std::endl;
436 for (size_t index = 0; index != expected_replies.size(); ++index) {
437 if (MatchWildcard(lines.back().c_str(), expected_replies[index])) {
438 if (g_netrocks_verbosity > 2) {
439 std::cerr << "Matched '" << expected_replies[index] << "' " << std::endl;
441 incoming.erase(incoming.begin(), incoming.begin() + right);
442 return index;
444 if (g_netrocks_verbosity > 2) {
445 std::cerr << "NOT Matched '" << expected_replies[index] << "' " << std::endl;
448 } else {
449 lines.pop_back();
451 if (right == incoming.size()) {
452 incoming.clear();
453 return -1;
455 } else {
456 ++right;
461 WaitResult WayToShell::SendAndWaitReplyInner(const std::string &send_str, const std::vector<const char *> &expected_replies)
463 if (g_netrocks_verbosity > 2) {
464 for (size_t index = 0; index != expected_replies.size(); ++index) {
465 std::cerr << "Expect '" << expected_replies[index] << "' " << std::endl;
468 WaitResult wr;
469 try {
470 for (bool first = true;;first = false) {
471 unsigned xrv;
472 if (first) {
473 xrv = STDOUT | STDERR;
474 if (!send_str.empty()) {
475 xrv|= Xfer(send_str.c_str(), send_str.size());
477 } else {
478 xrv = Xfer();
480 if (xrv & STDOUT) {
481 wr.index = DispatchExpectedReply(wr.stdout_lines, _stdout_data, expected_replies);
482 if (wr.index != -1) {
483 wr.output_type = STDOUT;
484 break;
487 if (xrv & STDERR) {
488 wr.index = DispatchExpectedReply(wr.stderr_lines, _stderr_data, expected_replies);
489 if (wr.index != -1) {
490 wr.output_type = STDERR;
491 break;
496 } catch (std::exception &e) {
497 std::string stderr_str;
498 AppendTrimmedLines(stderr_str, wr.stderr_lines);
499 if (!_stderr_data.empty()) {
500 stderr_str+= '\n';
501 stderr_str.append(_stderr_data.data(), _stderr_data.size());
503 if (stderr_str.empty()) {
504 throw;
506 throw ProtocolError(e.what(), stderr_str.c_str());
509 if (g_netrocks_verbosity > 0 && !wr.stdout_lines.empty()) {
510 for (const auto &line : wr.stdout_lines) {
511 DebugStr("STDOUT.RPL", line);
515 if (g_netrocks_verbosity > 0 && !wr.stderr_lines.empty()) {
516 for (const auto &line : wr.stderr_lines) {
517 DebugStr("STDERR.RPL", line);
521 return wr;
524 WaitResult WayToShell::SendAndWaitReply(const std::string &send_str, const std::vector<const char *> &expected_replies, bool hide_in_log)
526 if (g_netrocks_verbosity > 0) {
527 if (hide_in_log) {
528 DebugStr("SEND.HIDE", "???");
529 } else {
530 DebugStr("SEND", send_str);
534 return SendAndWaitReplyInner(send_str, expected_replies);
537 WaitResult WayToShell::WaitReply(const std::vector<const char *> &expected_replies)
539 return SendAndWaitReplyInner(std::string(), expected_replies);
542 void WayToShell::Send(const char *data, size_t len)
544 if (g_netrocks_verbosity > 1) {
545 DebugStr("SEND.BLOB", data, len);
547 } else if (g_netrocks_verbosity > 0) {
548 DebugStr("SEND.BLOB", StrPrintf("{%lu}", (unsigned long)len));
551 Xfer(data, len);
554 void WayToShell::Send(const char *data)
556 const size_t len = strlen(data);
557 if (g_netrocks_verbosity > 0) {
558 DebugStr("SEND.PSZ", data, len);
561 Xfer(data, len);
564 void WayToShell::Send(const std::string &line)
566 if (g_netrocks_verbosity > 0) {
567 DebugStr("SEND.STR", line);
570 Xfer(line.c_str(), line.size());
573 void WayToShell::ReadStdout(void *buffer, size_t len)
575 for (size_t ofs = 0;;) {
576 size_t piece = std::min(len - ofs, _stdout_data.size());
577 if (piece) {
578 memcpy((char *)buffer + ofs, _stdout_data.data(), piece);
579 _stdout_data.erase(_stdout_data.begin(), _stdout_data.begin() + piece);
580 ofs+= piece;
582 if (ofs == len) {
583 break;
585 RecvSomeStdout();
588 if (g_netrocks_verbosity > 1) {
589 DebugStr("STDOUT.BLOB", (const char *)buffer, len);
590 } else if (g_netrocks_verbosity > 0) {
591 const auto &info = StrPrintf("{%lu}", (unsigned long)len);
592 DebugStr("STDOUT.BLOB", info);
596 std::string MultiLineRequest(std::string line)
598 line+= '\n';
599 return line;