#ifndef _WIN32 #include "terminal_io.h" #include #include #include #include #include #include #include #include namespace crossdesk::cli { std::atomic interrupted{0}; namespace { void Signal(int value) { interrupted.store(value, std::memory_order_relaxed); } } // namespace void InitializeIo() { struct sigaction action{}; action.sa_handler = Signal; sigemptyset(&action.sa_mask); for (int sig : {SIGINT, SIGTERM, SIGHUP}) sigaction(sig, &action, nullptr); std::signal(SIGPIPE, SIG_IGN); } const char *Platform() { #ifdef __APPLE__ return "macos"; #else return "linux"; #endif } bool IsTty(int fd) { return isatty(fd); } std::ptrdiff_t Read(int fd, void *buffer, size_t size) { return read(fd, buffer, size); } std::ptrdiff_t Write(int fd, const void *buffer, size_t size) { return write(fd, buffer, size); } void Close(int fd) { close(fd); } int Duplicate(int fd) { return dup(fd); } int Redirect(int from, int to) { return dup2(from, to); } int OpenLog(const std::string &path) { return open(path.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0600); } bool WaitReadable(int fd, int timeout_ms) { pollfd p{fd, POLLIN, 0}; int result = poll(&p, 1, timeout_ms); if (result < 0 && errno != EINTR) throw std::runtime_error("cannot poll input descriptor"); if (p.revents & POLLNVAL) { // Darwin's /dev/tty may reject poll even though the descriptor is valid. // select supports that controlling-terminal device without a blocking read. if (fd < 0 || fd >= FD_SETSIZE || fcntl(fd, F_GETFL) < 0) throw std::runtime_error("input descriptor unavailable"); fd_set readers; FD_ZERO(&readers); FD_SET(fd, &readers); timeval timeout{timeout_ms / 1000, (timeout_ms % 1000) * 1000}; result = select(fd + 1, &readers, nullptr, nullptr, &timeout); if (result < 0 && errno != EINTR) throw std::runtime_error("cannot wait for terminal input"); return result > 0; } return result > 0 && (p.revents & (POLLIN | POLLHUP | POLLERR)); } void WaitIo(int input_fd, int output_fd, int timeout_ms) { pollfd fds[2] = {{input_fd, POLLIN, 0}, {output_fd, POLLOUT, 0}}; poll(fds, 2, timeout_ms); } TerminalSize GetTerminalSize(int fd) { winsize ws{}; TerminalSize size; if (ioctl(fd, TIOCGWINSZ, &ws) == 0 && ws.ws_col && ws.ws_row) { size.columns = ws.ws_col; size.rows = ws.ws_row; size.pixel_width = ws.ws_xpixel; size.pixel_height = ws.ws_ypixel; } return size; } struct NonBlocking::State { int fd, flags; }; NonBlocking::NonBlocking(int fd, bool) : state_(std::make_unique()) { state_->fd = fd; state_->flags = fcntl(fd, F_GETFL); if (state_->flags < 0 || fcntl(fd, F_SETFL, state_->flags | O_NONBLOCK) < 0) throw std::runtime_error("cannot configure nonblocking descriptor"); } NonBlocking::~NonBlocking() { fcntl(state_->fd, F_SETFL, state_->flags); } struct RawTerminal::State { int fd, flags; termios saved; }; RawTerminal::RawTerminal(int input, int) : state_(std::make_unique()) { state_->fd = input; state_->flags = fcntl(input, F_GETFL); if (state_->flags < 0 || tcgetattr(input, &state_->saved) != 0) throw std::runtime_error("cannot read terminal attributes"); termios mode = state_->saved; cfmakeraw(&mode); if (tcsetattr(input, TCSAFLUSH, &mode) != 0) throw std::runtime_error("cannot enter terminal raw mode"); if (fcntl(input, F_SETFL, state_->flags | O_NONBLOCK) < 0) { tcsetattr(input, TCSAFLUSH, &state_->saved); throw std::runtime_error("cannot configure terminal input"); } } RawTerminal::~RawTerminal() { tcsetattr(state_->fd, TCSAFLUSH, &state_->saved); fcntl(state_->fd, F_SETFL, state_->flags); } std::string Password() { int fd = open("/dev/tty", O_RDWR | O_NOCTTY); if (fd < 0) throw std::runtime_error("password requires an interactive TTY"); struct Restore { int fd; termios mode{}; bool active = false; ~Restore() { if (active) { tcsetattr(fd, TCSAFLUSH, &mode); write(fd, "\n", 1); } close(fd); } } restore{fd}; if (tcgetattr(fd, &restore.mode)) throw std::runtime_error("cannot read password TTY"); auto mode = restore.mode; mode.c_lflag &= ~ECHO; if (tcsetattr(fd, TCSAFLUSH, &mode)) throw std::runtime_error("cannot disable password echo"); restore.active = true; const char prompt[] = "Remote password: "; write(fd, prompt, sizeof(prompt) - 1); std::string result; while (!interrupted) { if (!WaitReadable(fd, 100)) continue; char c; auto n = read(fd, &c, 1); if (n < 0 && errno == EINTR) continue; if (n <= 0 || result.size() > 256) break; if (c == '\n' || c == '\r') return result; result += c; } throw std::runtime_error("password entry cancelled or too long"); } std::string Logs() { char path[] = "/tmp/crossdesk-cli-XXXXXX"; char *dir = mkdtemp(path); if (!dir) throw std::runtime_error("cannot create private log directory"); return dir; } } // namespace crossdesk::cli #endif