From 28643632ce46ffa165a60ab584b917628ef13863 Mon Sep 17 00:00:00 2001 From: Christian Burger Date: Sun, 3 Apr 2022 10:16:20 +0200 Subject: [PATCH] prototype: ncurses window class wrapping a PTY --- .gitignore | 2 + .vscode/launch.json | 33 +++++ .vscode/settings.json | 57 +++++++ CMakeLists.txt | 31 ++++ Debug.cpp | 337 ++++++++++++++++++++++++++++++++++++++++++ Debug.hpp | 52 +++++++ NCursesPtyApp.cpp | 41 +++++ NCursesPtyApp.hpp | 19 +++ NCursesPtyWindow.cpp | 283 +++++++++++++++++++++++++++++++++++ NCursesPtyWindow.hpp | 57 +++++++ cmake/gsl.cmake | 7 + cmake/ncurses.cmake | 12 ++ main.cpp | 9 ++ 13 files changed, 940 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt create mode 100644 Debug.cpp create mode 100644 Debug.hpp create mode 100644 NCursesPtyApp.cpp create mode 100644 NCursesPtyApp.hpp create mode 100644 NCursesPtyWindow.cpp create mode 100644 NCursesPtyWindow.hpp create mode 100644 cmake/gsl.cmake create mode 100644 cmake/ncurses.cmake create mode 100644 main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3e4ded --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +sandbox/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..83e446a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) cursesPty starten", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/NCursesPtyApp", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": true, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Automatische Strukturierung und Einrückung für \"gdb\" aktivieren", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Disassemblierungsvariante auf Intel festlegen", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ] + } + + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2847634 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,57 @@ +{ + "cmake.configureOnOpen": true, + "files.associations": { + "*.tcc": "cpp", + "fstream": "cpp", + "iosfwd": "cpp", + "istream": "cpp", + "limits": "cpp", + "sstream": "cpp", + "streambuf": "cpp", + "filesystem": "cpp", + "cstdio": "cpp", + "thread": "cpp", + "exception": "cpp", + "memory_resource": "cpp", + "new": "cpp", + "ostream": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "codecvt": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iostream": "cpp", + "stdexcept": "cpp", + "typeinfo": "cpp" + } +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..170bb26 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.16.3) +project(NCursesPty VERSION 0.1.0) + +set(CMAKE_CXX_STANDARD 17) + +include(CTest) +enable_testing() + +include("cmake/ncurses.cmake") +include("cmake/gsl.cmake") + + +### libraries +find_library(LIBVTERM_LIBRARY vterm) +find_library(UTIL_LIBRARY util) + +add_library(NCursesPty NCursesPtyWindow.cpp Debug.cpp) +target_link_libraries(NCursesPty ${CURSES_LIBRARIES} ${LIBVTERM_LIBRARY} ${UTIL_LIBRARY}) + +### threads +set(THREADS_PREFER_PTHREAD_FLAG true) +find_package(Threads REQUIRED) +target_link_libraries(NCursesPty Threads::Threads) + +### demo application +add_executable(NCursesPtyApp main.cpp NCursesPtyApp.cpp) +target_link_libraries(NCursesPtyApp ${CURSES_LIBRARIES} NCursesPty) + +set(CPACK_PROJECT_NAME ${PROJECT_NAME}) +set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) +include(CPack) diff --git a/Debug.cpp b/Debug.cpp new file mode 100644 index 0000000..0656570 --- /dev/null +++ b/Debug.cpp @@ -0,0 +1,337 @@ +/** + * @author Christian Burger (christian@krikkel.de) + * @todo Switch over to ? For resolving system call numbers? + * Maybe keep the current solution as a fallback? + * @todo catch `out of range` exception from stoi() + * + * Contains mapping of system calls numbers to names from original dev system: + * "Linux 5.13.0-28-generic #31~20.04.1-Ubuntu SMP \ + * Wed Jan 19 14:08:10 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux" + * See comment at the end for the actual command to gather mapping information. + */ + +#ifndef NDEBUG + +#include "Debug.hpp" + +#include +#include +#include + +#define BUFFER_SIZE 128 + +namespace krikkel +{ + using std::filesystem::is_directory; + + Debug::Debug() + { + string directory = "."; + if(is_directory("sandbox")) + directory = "./sandbox"; + logFile = fstream(directory + "/debug.log", fstream::out | fstream::app);//| fstream::trunc); + logFile << endl << endl << "New instance of class Debug." << endl; + logFile << getUname(); + logFile.flush(); + } + + string Debug::getUname() + { + string result = ""; + char buffer[BUFFER_SIZE]; + FILE *unameProcess = popen("uname -srvmpio", "r"); + + if(unameProcess && fgets(buffer, BUFFER_SIZE, unameProcess)) + result = buffer; + pclose(unameProcess); + + return result; + } + + string Debug::getTimestamp() + { + time_t now = std::time(NULL); + tm *localNow = localtime(&now); + static char formattedLocalNow[32]; + strftime(formattedLocalNow, 32, "%c", localNow); + + return formattedLocalNow; + } + + void Debug::log(string message, string fileName, int lineNo, string functionName) + { + string output = ""; + size_t position = message.find("sysCall("); + + if(position != string::npos) + { + size_t end = message.find_first_not_of("0123456789", position + 8); + unsigned long long systemCallNumber = std::stoi(message.substr(position + 8, end - position - 8)); + string systemCallName = strlen(syscalls[systemCallNumber]) != 0 ? syscalls[systemCallNumber] + : "unknown_system_call"; + if(systemCallNumber < MAX_NUMBER_OF_SYSCALLS) + output = message.replace(position, end - position + 1, systemCallName + "("); + } + if(output == "") + output = message; + + logFile << getTimestamp() << ": " << output << " (in " + << fileName.substr(fileName.rfind('/') + 1) << ":" + << lineNo << " in " << functionName << "())" << endl; + logFile.flush(); + } + + Debug *Debug::getInstance() + { + static Debug *debug = new Debug(); + return debug; + } + + const char *Debug::syscalls[MAX_NUMBER_OF_SYSCALLS] = {[0] = "read",[1] = "write", + [2] = "open",[3] = "close", + [4] = "stat",[5] = "fstat", + [6] = "lstat",[7] = "poll", + [8] = "lseek",[9] = "mmap", + [10] = "mprotect",[11] = "munmap", + [12] = "brk",[13] = "rt_sigaction", + [14] = "rt_sigprocmask",[15] = "rt_sigreturn", + [16] = "ioctl",[17] = "pread64", + [18] = "pwrite64",[19] = "readv", + [20] = "writev",[21] = "access", + [22] = "pipe",[23] = "select", + [24] = "sched_yield",[25] = "mremap", + [26] = "msync",[27] = "mincore", + [28] = "madvise",[29] = "shmget", + [30] = "shmat",[31] = "shmctl", + [32] = "dup",[33] = "dup2", + [34] = "pause",[35] = "nanosleep", + [36] = "getitimer",[37] = "alarm", + [38] = "setitimer",[39] = "getpid", + [40] = "sendfile",[41] = "socket", + [42] = "connect",[43] = "accept", + [44] = "sendto",[45] = "recvfrom", + [46] = "sendmsg",[47] = "recvmsg", + [48] = "shutdown",[49] = "bind", + [50] = "listen",[51] = "getsockname", + [52] = "getpeername",[53] = "socketpair", + [54] = "setsockopt",[55] = "getsockopt", + [56] = "clone",[57] = "fork", + [58] = "vfork",[59] = "execve", + [60] = "exit",[61] = "wait4", + [62] = "kill",[63] = "uname", + [64] = "semget",[65] = "semop", + [66] = "semctl",[67] = "shmdt", + [68] = "msgget",[69] = "msgsnd", + [70] = "msgrcv",[71] = "msgctl", + [72] = "fcntl",[73] = "flock", + [74] = "fsync",[75] = "fdatasync", + [76] = "truncate",[77] = "ftruncate", + [78] = "getdents",[79] = "getcwd", + [80] = "chdir",[81] = "fchdir", + [82] = "rename",[83] = "mkdir", + [84] = "rmdir",[85] = "creat", + [86] = "link",[87] = "unlink", + [88] = "symlink",[89] = "readlink", + [90] = "chmod",[91] = "fchmod", + [92] = "chown",[93] = "fchown", + [94] = "lchown",[95] = "umask", + [96] = "gettimeofday",[97] = "getrlimit", + [98] = "getrusage",[99] = "sysinfo", + [100] = "times",[101] = "ptrace", + [102] = "getuid",[103] = "syslog", + [104] = "getgid",[105] = "setuid", + [106] = "setgid",[107] = "geteuid", + [108] = "getegid",[109] = "setpgid", + [110] = "getppid",[111] = "getpgrp", + [112] = "setsid",[113] = "setreuid", + [114] = "setregid",[115] = "getgroups", + [116] = "setgroups",[117] = "setresuid", + [118] = "getresuid",[119] = "setresgid", + [120] = "getresgid",[121] = "getpgid", + [122] = "setfsuid",[123] = "setfsgid", + [124] = "getsid",[125] = "capget", + [126] = "capset",[127] = "rt_sigpending", + [128] = "rt_sigtimedwait",[129] = "rt_sigqueueinfo", + [130] = "rt_sigsuspend",[131] = "sigaltstack", + [132] = "utime",[133] = "mknod", + [134] = "uselib",[135] = "personality", + [136] = "ustat",[137] = "statfs", + [138] = "fstatfs",[139] = "sysfs", + [140] = "getpriority",[141] = "setpriority", + [142] = "sched_setparam",[143] = "sched_getparam", + [144] = "sched_setscheduler",[145] = "sched_getscheduler", + [146] = "sched_get_priority_max",[147] = "sched_get_priority_min", + [148] = "sched_rr_get_interval",[149] = "mlock", + [150] = "munlock",[151] = "mlockall", + [152] = "munlockall",[153] = "vhangup", + [154] = "modify_ldt",[155] = "pivot_root", + [156] = "",[157] = "prctl", + [158] = "arch_prctl",[159] = "adjtimex", + [160] = "setrlimit",[161] = "chroot", + [162] = "sync",[163] = "acct", + [164] = "settimeofday",[165] = "mount", + [166] = "",[167] = "swapon", + [168] = "swapoff",[169] = "reboot", + [170] = "sethostname",[171] = "setdomainname", + [172] = "iopl",[173] = "ioperm", + [174] = "",[175] = "init_module", + [176] = "delete_module",[177] = "", + [178] = "",[179] = "quotactl", + [180] = "",[181] = "", + [182] = "",[183] = "", + [184] = "",[185] = "", + [186] = "gettid",[187] = "readahead", + [188] = "setxattr",[189] = "lsetxattr", + [190] = "fsetxattr",[191] = "getxattr", + [192] = "lgetxattr",[193] = "fgetxattr", + [194] = "listxattr",[195] = "llistxattr", + [196] = "flistxattr",[197] = "removexattr", + [198] = "lremovexattr",[199] = "fremovexattr", + [200] = "tkill",[201] = "time", + [202] = "futex",[203] = "sched_setaffinity", + [204] = "sched_getaffinity",[205] = "set_thread_area", + [206] = "io_setup",[207] = "io_destroy", + [208] = "io_getevents",[209] = "io_submit", + [210] = "io_cancel",[211] = "get_thread_area", + [212] = "",[213] = "epoll_create", + [214] = "",[215] = "", + [216] = "remap_file_pages",[217] = "getdents64", + [218] = "set_tid_address",[219] = "restart_syscall", + [220] = "semtimedop",[221] = "fadvise64", + [222] = "timer_create",[223] = "timer_settime", + [224] = "timer_gettime",[225] = "timer_getoverrun", + [226] = "timer_delete",[227] = "clock_settime", + [228] = "clock_gettime",[229] = "clock_getres", + [230] = "clock_nanosleep",[231] = "exit_group", + [232] = "epoll_wait",[233] = "epoll_ctl", + [234] = "tgkill",[235] = "utimes", + [236] = "",[237] = "mbind", + [238] = "set_mempolicy",[239] = "get_mempolicy", + [240] = "mq_open",[241] = "mq_unlink", + [242] = "mq_timedsend",[243] = "mq_timedreceive", + [244] = "mq_notify",[245] = "mq_getsetattr", + [246] = "kexec_load",[247] = "waitid", + [248] = "add_key",[249] = "request_key", + [250] = "keyctl",[251] = "ioprio_set", + [252] = "ioprio_get",[253] = "inotify_init", + [254] = "inotify_add_watch",[255] = "inotify_rm_watch", + [256] = "migrate_pages",[257] = "openat", + [258] = "mkdirat",[259] = "mknodat", + [260] = "fchownat",[261] = "futimesat", + [262] = "newfstatat",[263] = "unlinkat", + [264] = "renameat",[265] = "linkat", + [266] = "symlinkat",[267] = "readlinkat", + [268] = "fchmodat",[269] = "faccessat", + [270] = "pselect6",[271] = "ppoll", + [272] = "unshare",[273] = "set_robust_list", + [274] = "get_robust_list",[275] = "splice", + [276] = "tee",[277] = "sync_file_range", + [278] = "vmsplice",[279] = "move_pages", + [280] = "utimensat",[281] = "epoll_pwait", + [282] = "signalfd",[283] = "timerfd_create", + [284] = "eventfd",[285] = "fallocate", + [286] = "timerfd_settime",[287] = "timerfd_gettime", + [288] = "accept4",[289] = "signalfd4", + [290] = "eventfd2",[291] = "epoll_create1", + [292] = "dup3",[293] = "pipe2", + [294] = "inotify_init1",[295] = "preadv", + [296] = "pwritev",[297] = "rt_tgsigqueueinfo", + [298] = "perf_event_open",[299] = "recvmmsg", + [300] = "fanotify_init",[301] = "fanotify_mark", + [302] = "prlimit64",[303] = "name_to_handle_at", + [304] = "open_by_handle_at",[305] = "clock_adjtime", + [306] = "syncfs",[307] = "sendmmsg", + [308] = "setns",[309] = "getcpu", + [310] = "process_vm_readv",[311] = "process_vm_writev", + [312] = "kcmp",[313] = "finit_module", + [314] = "sched_setattr",[315] = "sched_getattr", + [316] = "renameat2",[317] = "seccomp", + [318] = "getrandom",[319] = "memfd_create", + [320] = "kexec_file_load",[321] = "bpf", + [322] = "execveat",[323] = "userfaultfd", + [324] = "membarrier",[325] = "mlock2", + [326] = "copy_file_range",[327] = "preadv2", + [328] = "pwritev2",[329] = "pkey_mprotect", + [330] = "pkey_alloc",[331] = "pkey_free", + [332] = "statx",[333] = "io_pgetevents", + [334] = "rseq",[335] = "", + [336] = "",[337] = "", + [338] = "",[339] = "", + [340] = "",[341] = "", + [342] = "",[343] = "", + [344] = "",[345] = "", + [346] = "",[347] = "", + [348] = "",[349] = "", + [350] = "",[351] = "", + [352] = "",[353] = "", + [354] = "",[355] = "", + [356] = "",[357] = "", + [358] = "",[359] = "", + [360] = "",[361] = "", + [362] = "",[363] = "", + [364] = "",[365] = "", + [366] = "",[367] = "", + [368] = "",[369] = "", + [370] = "",[371] = "", + [372] = "",[373] = "", + [374] = "",[375] = "", + [376] = "",[377] = "", + [378] = "",[379] = "", + [380] = "",[381] = "", + [382] = "",[383] = "", + [384] = "",[385] = "", + [386] = "",[387] = "", + [388] = "",[389] = "", + [390] = "",[391] = "", + [392] = "",[393] = "", + [394] = "",[395] = "", + [396] = "",[397] = "", + [398] = "",[399] = "", + [400] = "",[401] = "", + [402] = "",[403] = "", + [404] = "",[405] = "", + [406] = "",[407] = "", + [408] = "",[409] = "", + [410] = "",[411] = "", + [412] = "",[413] = "", + [414] = "",[415] = "", + [416] = "",[417] = "", + [418] = "",[419] = "", + [420] = "",[421] = "", + [422] = "",[423] = "", + [424] = "pidfd_send_signal",[425] = "io_uring_setup", + [426] = "io_uring_enter",[427] = "io_uring_register", + [428] = "open_tree",[429] = "move_mount", + [430] = "fsopen",[431] = "fsconfig", + [432] = "fsmount",[433] = "fspick", + [434] = "pidfd_open",[435] = "clone3" }; +} + +#endif +/* + +Following command in part thx to: +https://unix.stackexchange.com/questions/445507/syscall-number-%E2%86%92-name-mapping-at-runtime + +Command to create mapping of system call number to system call name: + +#!/bin/bash + +awk 'BEGIN { print "#include " } + /p_syscall_meta/ { syscall = substr($NF, 19); + printf "[SYS_%s] = \"%s\", \n", syscall, syscall }' /proc/kallsyms \ +| gcc -E -P - \ +| sort -V \ +| grep "\[[0-9]" \ +| awk 'BEGIN {expectedIndex = 0;} + { actualIndex = $1; + gsub(/[\[\]]/, "", actualIndex); + if (actualIndex != expectedIndex) + for (; expectedIndex < actualIndex; expectedIndex++) + print "[" expectedIndex "] = \"\","; + print $0; expectedIndex++ }' \ +| tr -d "\n" \ +| sed -e "s/^/const char *syscalls[1024] = {/; s/,$/ };/" \ +| sed -e 's/,/,\n /2;P;D' \ +| cat - <(echo) # new line at the end +*/ \ No newline at end of file diff --git a/Debug.hpp b/Debug.hpp new file mode 100644 index 0000000..4687622 --- /dev/null +++ b/Debug.hpp @@ -0,0 +1,52 @@ +/** + * @brief Writes messages to `debug.log` if `NDEBUG` is not defined. + * @author Christian Burger (christian@krikkel.de) + */ + +#ifndef __DEBUG_H__ +#define __DEBUG_H__ + +#ifndef NDEBUG + +#include +#include +#include + +namespace krikkel +{ + using std::fstream; + using std::string; + using std::endl; + using std::strftime; + using std::localtime; + using std::time; + + class Debug + { + private: + fstream logFile; + static const int MAX_NUMBER_OF_SYSCALLS = 1024; + static const char * syscalls[MAX_NUMBER_OF_SYSCALLS]; + + Debug(); + string getTimestamp(); + string getUname(); + + public: + void log(string message, string fileName, int lineNo + , string functionName); + static Debug *getInstance(); + }; +} + +#define __debug_log(message) krikkel::Debug::getInstance()\ + ->log(message, __FILE__, __LINE__, __func__) + +#else + +/// @todo check what's the business with this "((void)0)" instead of empty macro +#define __debug_log(message) + +#endif /* NDEBUG */ + +#endif // __DEBUG_H__ \ No newline at end of file diff --git a/NCursesPtyApp.cpp b/NCursesPtyApp.cpp new file mode 100644 index 0000000..dad7a74 --- /dev/null +++ b/NCursesPtyApp.cpp @@ -0,0 +1,41 @@ +/** + * @author Christian Burger (christian@krikkel.de) + */ + +#include "NCursesPtyApp.hpp" +#include "NCursesPtyWindow.hpp" +#include "Debug.hpp" + +#include +#include + +namespace krikkel::NCursesPty +{ + int NCursesPtyApp::run() + { + NCursesPtyWindow *ptyWindow = new NCursesPtyWindow( + Root_Window->lines() + , Root_Window->cols() + , 0 + , 0); + + if(fork() == 0) + { + /// @todo close(ptyWindow->getFdPtyHost()); ??? + login_tty(ptyWindow->getFdPtyClient()); + const int sizeArg = 2; + const char *arg[sizeArg] = {"/bin/bash", NULL}; + + execv("/bin/bash", const_cast(arg)); + } + + int input; + while((input = ptyWindow->getch()) != 27) + { + __debug_log("input: " + std::to_string(input)); + ptyWindow->writeToClient((unsigned char) input); + } + + return 0; + } +} diff --git a/NCursesPtyApp.hpp b/NCursesPtyApp.hpp new file mode 100644 index 0000000..968b556 --- /dev/null +++ b/NCursesPtyApp.hpp @@ -0,0 +1,19 @@ +/** + * @brief demo application for the library + * @author Christian Burger (christian@krikkel.de) + */ + +#ifndef A3B2AE4E_0A39_468C_8CCA_E6508166702A +#define A3B2AE4E_0A39_468C_8CCA_E6508166702A + +#include + +namespace krikkel::NCursesPty +{ + class NCursesPtyApp : public NCursesApplication + { + int run() override; + }; +} + +#endif /* A3B2AE4E_0A39_468C_8CCA_E6508166702A */ diff --git a/NCursesPtyWindow.cpp b/NCursesPtyWindow.cpp new file mode 100644 index 0000000..c994463 --- /dev/null +++ b/NCursesPtyWindow.cpp @@ -0,0 +1,283 @@ +/** + * @author Christian Burger (christian@krikkel.de) + */ + +#include "NCursesPtyWindow.hpp" +#include "Debug.hpp" + +#include +#include +#include +#include +#include +#include + +namespace krikkel::NCursesPty +{ + using gsl::narrow; + + NCursesPtyWindow::NCursesPtyWindow(int lines, int columns, int y, int x) + : NCursesWindow(lines, columns, y, x) + { + // to get the original terminal we need to shutdown ncurses for a moment + ::endwin(); + tcgetattr(STDIN_FILENO, &terminalParameters); + ::refresh(); + + winsize windowSize = + { + .ws_row = narrow(this->height()) + , .ws_col = narrow(this->width()) + }; + openpty(&fdPtyHost, &fdPtyClient, NULL, &terminalParameters, &windowSize); + + pseudoTerminal = vterm_new(windowSize.ws_row, windowSize.ws_col); + __debug_log("window size (x: " + std::to_string(windowSize.ws_col) + ", y: " + std::to_string(windowSize.ws_row) + ")"); + vterm_set_utf8(pseudoTerminal, true); + pseudoTerminalScreen = vterm_obtain_screen(pseudoTerminal); + vterm_screen_reset(pseudoTerminalScreen, true); + vterm_screen_set_callbacks(pseudoTerminalScreen, &screenCallbacks, this); + + // the terminal is doing most of the work + keypad(false); + + /// @todo block all signals, this thread does not handle any + readPtyClientThread = std::thread(&NCursesPtyWindow::readFromPtyClientThreadMethod, this); + } + + NCursesPtyWindow::~NCursesPtyWindow() + { + close(fdPtyHost); + close(fdPtyClient); + vterm_free(pseudoTerminal); + } + + int NCursesPtyWindow::getFdPtyClient() const + { + return fdPtyClient; + } + + /// @todo maybe implement a function with a string buffer + void NCursesPtyWindow::writeToClient(char character) + { + //__debug_log(std::string("written '") + character + "' to client"); + write(fdPtyHost, &character, sizeof(character)); + } + + void NCursesPtyWindow::readFromPtyClientThreadMethod() + { + /// @todo in theory, there is no need for a timeout or select … + /// file descriptor is blocking + while(1) + { + readFromPtyClient(); + struct timeval timeout = { .tv_sec = 0, .tv_usec = 200000 }; + fd_set readFds; + + FD_ZERO(&readFds); + FD_SET(fdPtyHost, &readFds); + if(select(fdPtyHost + 1, &readFds, NULL, NULL, &timeout) < 0) + break; + } + } + + void NCursesPtyWindow::readFromPtyClient() + { + size_t bytesRead = read(fdPtyHost, clientOutputBuffer, CLIENT_OUTPUT_BUFFER_SIZE); + if(bytesRead) + vterm_input_write(pseudoTerminal, clientOutputBuffer, bytesRead); + __debug_log("read " + std::to_string(bytesRead) + " bytes from PTY client"); + /*string readable; + for(char character : clientOutputBuffer) + if(std::isprint((unsigned char) character)) + readable += character; + else + readable += "\\" + std::to_string((int) character) + "/"; + __debug_log("read: '" + readable + "'");*/ + } + + VTermScreenCallbacks NCursesPtyWindow::screenCallbacks = + { + .damage = staticHandlerDamage, +/* .moverect = staticHandlerMoveRect, + .movecursor = staticHandlerMoveCursor, + .settermprop = staticHandlerSetTermProp, + .bell = staticHandlerBell, + .resize = staticHandlerResize, + .sb_pushline = staticHandlerPushLine, + .sb_popline = staticHandlerPopLine,*/ + }; + + int NCursesPtyWindow::handlerDamage(VTermRect rect) + { + __debug_log("damage to rectangle (" + + std::to_string(rect.start_col) + ", " + + std::to_string(rect.start_row) + ", " + + std::to_string(rect.end_col) + ", " + + std::to_string(rect.end_row) + ") " + + "size: " + std::to_string((rect.start_col - rect.end_col) * (rect.start_row - rect.end_row)) ); + + int cursorX, cursorY; + getyx(cursorY, cursorX); + + for(int x = rect.start_col; x < rect.end_col; ++x) + for(int y = rect.start_row; y < rect.end_row; ++y) + copyPtyCellToNCursesWindow(x, y); + + move(cursorY, cursorX); + + return 0; + } + + int NCursesPtyWindow::handlerMoveRect(VTermRect dest, VTermRect src) + { + __debug_log("(unimplemented) move content in rectangle from (" + + std::to_string(src.start_col) + ", " + + std::to_string(src.start_row) + ", " + + std::to_string(src.end_col) + ", " + + std::to_string(src.end_row) + ") " + + "size: " + std::to_string((src.start_col - src.end_col) * (src.start_row - src.end_row)) + + " to (" + + std::to_string(dest.start_col) + ", " + + std::to_string(dest.start_row) + ", " + + std::to_string(dest.end_col) + ", " + + std::to_string(dest.end_row) + ") " + + "size: " + std::to_string((dest.start_col - dest.end_col) * (dest.start_row - dest.end_row))); + return 0; + } + + int NCursesPtyWindow::handlerMoveCursor(VTermPos pos, VTermPos oldpos, int visible) + { + __debug_log("cursor moved to (" + + std::to_string(pos.col) + ", " + + std::to_string(pos.row) + ") " + + "visible: " + (visible ? "true" : "false")); + move(pos.row, pos.col); + return 0; + } + + int NCursesPtyWindow::handlerSetTermProp(VTermProp prop, VTermValue *val) + { + + __debug_log(std::string("(unimplemented) set terminal property: ") + + (prop == VTERM_PROP_CURSORVISIBLE + ? std::string("cursor visible = ") + + (val->boolean ? "true" : "false") + : "") + + (prop == VTERM_PROP_CURSORBLINK + ? std::string("cursor blink = ") + + (val->boolean ? "true" : "false") + : "") + + (prop == VTERM_PROP_ALTSCREEN + ? std::string("alt screen = ") + + (val->boolean ? "true" : "false") + : "") + + (prop == VTERM_PROP_TITLE + ? std::string("title = ") + + val->string + : "") + + (prop == VTERM_PROP_ICONNAME + ? std::string("icon name = ") + + val->string + : "") + + (prop == VTERM_PROP_REVERSE + ? std::string("alt screen = ") + + (val->boolean ? "true" : "false") + : "") + + (prop == VTERM_PROP_CURSORSHAPE + ? std::string("icon name = ") + + std::to_string(val->number) + : "") + + (prop == VTERM_PROP_MOUSE + ? std::string("mouse = ") + + std::to_string(val->number) + : "") + + (prop == VTERM_N_PROPS + ? "n props" : "") + ); + return 0; + } + + int NCursesPtyWindow::handlerBell() + { + beep(); + return 0; + } + + int NCursesPtyWindow::handlerResize(int rows, int cols) + { + __debug_log("unimplemented handler called"); + return 0; + } + + int NCursesPtyWindow::handlerPushLine(int cols, const VTermScreenCell *cells) + { + __debug_log("(unimplemented) push line with " + std::to_string(cols) + " columns"); + return 0; + } + + int NCursesPtyWindow::handlerPopLine(int cols, VTermScreenCell *cells) + { + __debug_log("unimplemented handler called"); + return 0; + } + + void NCursesPtyWindow::copyPtyCellToNCursesWindow(int x, int y) + { + VTermPos cellPosition = {y, x}; + VTermScreenCell cell; + vterm_screen_get_cell(pseudoTerminalScreen, cellPosition, &cell); + move(y, x); + addch(cell.chars[0] ? cell.chars[0] : ' ' ); + /// @todo boy, is this expensive … + refresh(); + } + + int NCursesPtyWindow::staticHandlerDamage(VTermRect rect, void *user) + { + NCursesPtyWindow *instance = static_cast(user); + return instance->handlerDamage(rect); + } + + int NCursesPtyWindow::staticHandlerMoveRect(VTermRect dest, VTermRect src, void *user) + { + NCursesPtyWindow *instance = static_cast(user); + return instance->handlerMoveRect(dest, src); + } + + int NCursesPtyWindow::staticHandlerMoveCursor(VTermPos pos, VTermPos oldpos, int visible, void *user) + { + NCursesPtyWindow *instance = static_cast(user); + return instance->handlerMoveCursor(pos, oldpos, visible); + } + + int NCursesPtyWindow::staticHandlerSetTermProp(VTermProp prop, VTermValue *val, void *user) + { + NCursesPtyWindow *instance = static_cast(user); + return instance->handlerSetTermProp(prop, val); + } + + int NCursesPtyWindow::staticHandlerBell(void *user) + { + NCursesPtyWindow *instance = static_cast(user); + return instance->handlerBell(); + } + + int NCursesPtyWindow::staticHandlerResize(int rows, int cols, void *user) + { + NCursesPtyWindow *instance = static_cast(user); + return instance->handlerResize(rows, cols); + } + + int NCursesPtyWindow::staticHandlerPushLine(int cols, const VTermScreenCell *cells, void *user) + { + NCursesPtyWindow *instance = static_cast(user); + return instance->handlerPushLine(cols, cells); + } + + int NCursesPtyWindow::staticHandlerPopLine(int cols, VTermScreenCell *cells, void *user) + { + NCursesPtyWindow *instance = static_cast(user); + return instance->handlerPopLine(cols, cells); + } +} \ No newline at end of file diff --git a/NCursesPtyWindow.hpp b/NCursesPtyWindow.hpp new file mode 100644 index 0000000..592b154 --- /dev/null +++ b/NCursesPtyWindow.hpp @@ -0,0 +1,57 @@ +/** + * @brief `ncurses` window displaying contents of a pseudo terminal + * @author Christian Burger (christian@krikkel.de) + */ + +#include +#include +#include +#include +#include + +namespace krikkel::NCursesPty +{ + class NCursesPtyWindow : public NCursesWindow + { + public: + NCursesPtyWindow(int lines, int columns, int y, int x); + ~NCursesPtyWindow(); + + int getFdPtyClient() const; + void writeToClient(char character); + + private: + int fdPtyHost, fdPtyClient; + struct termios terminalParameters; + VTerm *pseudoTerminal; + VTermScreen *pseudoTerminalScreen; + static VTermScreenCallbacks screenCallbacks; + /// @todo one line is at most 4096 chars long + static const u_int CLIENT_OUTPUT_BUFFER_SIZE = 512; + char clientOutputBuffer[CLIENT_OUTPUT_BUFFER_SIZE]; + + std::thread readPtyClientThread; + void readFromPtyClientThreadMethod(); + void readFromPtyClient(); + + int handlerDamage(VTermRect rect); + int handlerMoveRect(VTermRect dest, VTermRect src); + int handlerMoveCursor(VTermPos pos, VTermPos oldpos, int visible); + int handlerSetTermProp(VTermProp prop, VTermValue *val); + int handlerBell(); + int handlerResize(int rows, int cols); + int handlerPushLine(int cols, const VTermScreenCell *cells); + int handlerPopLine(int cols, VTermScreenCell *cells); + + void copyPtyCellToNCursesWindow(int x, int y); + + static int staticHandlerDamage(VTermRect rect, void *user); + static int staticHandlerMoveRect(VTermRect dest, VTermRect src, void *user); + static int staticHandlerMoveCursor(VTermPos pos, VTermPos oldpos, int visible, void *user); + static int staticHandlerSetTermProp(VTermProp prop, VTermValue *val, void *user); + static int staticHandlerBell(void *user); + static int staticHandlerResize(int rows, int cols, void *user); + static int staticHandlerPushLine(int cols, const VTermScreenCell *cells, void *user); + static int staticHandlerPopLine(int cols, VTermScreenCell *cells, void *user); + }; +} \ No newline at end of file diff --git a/cmake/gsl.cmake b/cmake/gsl.cmake new file mode 100644 index 0000000..f55f87b --- /dev/null +++ b/cmake/gsl.cmake @@ -0,0 +1,7 @@ +find_path(GSL_INCLUDE_DIR "gsl") +if(GSL_INCLUDE_DIR STREQUAL "GSL_INCLUDE_DIR-NOTFOUND") + message(SEND_ERROR "Microsoft GSL not found.") +else() + message(STATUS "Microsoft GSL found.") + include_directories(SYSTEM ${GSL_INCLUDE_DIR}) +endif() \ No newline at end of file diff --git a/cmake/ncurses.cmake b/cmake/ncurses.cmake new file mode 100644 index 0000000..1069824 --- /dev/null +++ b/cmake/ncurses.cmake @@ -0,0 +1,12 @@ +### ncurses +set(CURSES_NEED_NCURSES TRUE) +set(CURSES_NEED_WIDE TRUE) +find_package(Curses 6.2 REQUIRED) +include_directories(${CURSES_INCLUDE_DIRS}) + +# find C++ interface for ncurses with unicode support +find_library(CURSES_CPP_WIDE_LIBRARY NAMES ncurses++w REQUIRED) +if(NOT CURSES_CPP_WIDE_LIBRARY) + message(FATAL_ERROR "C++ interface for ncurses (wide/unicode) not found.") +endif() +list(APPEND CURSES_LIBRARIES ${CURSES_CPP_WIDE_LIBRARY}) \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..fa5b984 --- /dev/null +++ b/main.cpp @@ -0,0 +1,9 @@ +/** + * @brief Instantiating class for demo application. `main()` is supplied by the + * `ncurses` library. + * @author Christian Burger (christian@krikkel.de) + */ + +#include "NCursesPtyApp.hpp" + +krikkel::NCursesPty::NCursesPtyApp cursesPtyApp; \ No newline at end of file