From 2863c8ae16a3002fe760e6d4c0daad46f0d3d164 Mon Sep 17 00:00:00 2001 From: Christian Burger Date: Sat, 23 Apr 2022 22:56:47 +0200 Subject: [PATCH] first release v0.1 Basic pseudo terminal capabilities in an ncurses window provided. Can run at least `nano`, `top` and `sudo`. Probably a lot more. No colors, yet. --- App.cpp | 63 +++ App.hpp | 23 + CMakeLists.txt | 62 +++ Debug.cpp | 503 +++++++++++++++++++ Debug.hpp | 132 +++++ LICENSE | 21 + README.md | 21 + SingleUserInput.cpp | 105 ++++ Window.cpp | 345 +++++++++++++ cmake/gsl.cmake | 11 + cmake/libvterm.cmake | 41 ++ cmake/ncurses.cmake | 15 + cmake/version.cmake | 77 +++ include/NCursesPtyWindow/SingleUserInput.hpp | 31 ++ include/NCursesPtyWindow/Window.hpp | 91 ++++ main.cpp | 9 + 16 files changed, 1550 insertions(+) create mode 100644 App.cpp create mode 100644 App.hpp create mode 100644 CMakeLists.txt create mode 100644 Debug.cpp create mode 100644 Debug.hpp create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SingleUserInput.cpp create mode 100644 Window.cpp create mode 100644 cmake/gsl.cmake create mode 100644 cmake/libvterm.cmake create mode 100644 cmake/ncurses.cmake create mode 100644 cmake/version.cmake create mode 100644 include/NCursesPtyWindow/SingleUserInput.hpp create mode 100644 include/NCursesPtyWindow/Window.hpp create mode 100644 main.cpp diff --git a/App.cpp b/App.cpp new file mode 100644 index 0000000..d471c5d --- /dev/null +++ b/App.cpp @@ -0,0 +1,63 @@ +/** + * @author Christian Burger (christian@krikkel.de) + */ + +#include "App.hpp" +#include "Debug.hpp" +#include + +#include +#include +#include + +namespace krikkel::NCursesPtyWindow +{ + App::App() : NCursesApplication(false) + { + + } + + int App::run() + { + std::mutex writeMutex; + + Window *ptyWindow = new Window(&writeMutex + , Root_Window->lines() + , Root_Window->cols() + , 0 + , 0); + + if(fork() == 0) + { + const char *shellPath = getenv("SHELL"); + if(!shellPath) + shellPath = "/bin/bash"; + + login_tty(ptyWindow->getFdPtyClient()); + const int sizeArg = 2; + const char *arg[sizeArg] = {shellPath, NULL}; + fprintf(stderr, "+C exits shell.\n\n"); + execv(shellPath, const_cast(arg)); + fprintf(stderr, "Well, well, well … could not start a shell. We " + "tried `%s`. Maybe set `SHELL` environment variable" + " to a working value?", shellPath); + exit(1); + } + + while(true) + { + SingleUserInput input = ptyWindow->readSingleUserInput(); + if(input.isNormalKey()) + ptyWindow->writeUnicodeCharToClient(input.getRawInput()); + if(input.isFunctionKey()) + { + if(input.getRawInput() == KEY_RESIZE) + ptyWindow->wresize(Root_Window->lines(), Root_Window->cols()); + else + ptyWindow->writeKeyToClient(input.mapKeyNcursesToVTerm()); + } + } + + return 0; + } +} diff --git a/App.hpp b/App.hpp new file mode 100644 index 0000000..b3174fe --- /dev/null +++ b/App.hpp @@ -0,0 +1,23 @@ +/** + * @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::NCursesPtyWindow +{ + class App : public NCursesApplication + { + public: + App(); + + private: + int run() override; + }; +} + +#endif /* A3B2AE4E_0A39_468C_8CCA_E6508166702A */ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c094603 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,62 @@ +## +# @author Christian Burger + +cmake_minimum_required(VERSION 3.16.3) + +include("cmake/version.cmake") +project(NCursesPtyWindow + HOMEPAGE_URL "https://gitea.xndr.de/christian/NCursesPtyWindow" + VERSION ${SEMANTIC_VERSION}) + +set(CMAKE_CXX_STANDARD 17) + +include(CTest) +enable_testing() + +add_library(NCursesPtyWindow Window.cpp SingleUserInput.cpp Debug.cpp) + +### path to own system includes +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/include") + +### libraries + +include("cmake/ncurses.cmake") +target_link_libraries(NCursesPtyWindow ${CURSES_LIBRARIES}) + +include("cmake/gsl.cmake") + +include("cmake/libvterm.cmake") +if(EXISTS libvtermProject) + add_dependencies(NCursesPtyWindow libvtermProject) +endif() +target_link_libraries(NCursesPtyWindow ${LIBVTERM_LIBRARY}) + +find_library(UTIL_LIBRARY util) +target_link_libraries(NCursesPtyWindow ${UTIL_LIBRARY}) + +set(THREADS_PREFER_PTHREAD_FLAG true) +find_package(Threads REQUIRED) +target_link_libraries(NCursesPtyWindow Threads::Threads) + +### demo application +add_executable(NCursesPtyApp main.cpp App.cpp) +target_link_libraries(NCursesPtyApp NCursesPtyWindow) + +### installation and packaging +set(NCURSES_PTY_WINDOW_SYSTEM_INCLUDE "include/NCursesPtyWindow") +set_target_properties(NCursesPtyWindow PROPERTIES PUBLIC_HEADER "${NCURSES_PTY_WINDOW_SYSTEM_INCLUDE}/Window.hpp;${NCURSES_PTY_WINDOW_SYSTEM_INCLUDE}/SingleUserInput.hpp" + VERSION "${CMAKE_PROJECT_VERSION}") +include(GNUInstallDirs) +install(TARGETS NCursesPtyWindow ARCHIVE + PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/NCursesPtyWindow/") + +set(CPACK_PROJECT_NAME ${PROJECT_NAME}) +set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) +set(CPACK_PACKAGE_CONTACT "Christian Burger ") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Library for a pseudo terminal where the host end is a ncurses window") +# 3.16.3 does not pick up the project's homepage URL by itself +set(CPACK_DEBIAN_PACKAGE_HOMEPAGE ${CMAKE_PROJECT_HOMEPAGE_URL}) + +set(CPACK_GENERATOR "DEB" "TGZ") +set(CPACK_DEBIAN_PACKAGE_DEPENDS "libncursesw6 (>=6.2), libvterm0 (>= 0.1.2)") +include(CPack) diff --git a/Debug.cpp b/Debug.cpp new file mode 100644 index 0000000..25d4f1a --- /dev/null +++ b/Debug.cpp @@ -0,0 +1,503 @@ +/** + * @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 +#include +#include +#include +#include +#include + +#define BUFFER_SIZE 128 + +namespace krikkel +{ + using std::string; + using std::fstream; + using std::filesystem::is_directory; + using std::endl; + using std::time; + using std::localtime; + using std::strftime; + using std::min; + + Debug::Debug() + { + string directory = "."; + if(is_directory("sandbox")) + directory = "./sandbox"; + logFile = fstream(directory + "/debug.log", fstream::out | fstream::app);//| fstream::trunc); + logFile << endl << endl << 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 = 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(); + } + + std::string Debug::peekData(pid_t shellPid, void *data, size_t length) + { + std::string result; + + for(int index = 0; index < length / sizeof(long); ++index) + { + long datum = ptrace(PTRACE_PEEKDATA, shellPid, ((char *) data) + index * sizeof(long)); + if(length == -1) + { + size_t datumLength = strnlen((char *) &datum, sizeof(long)); + result += string((char *) &datum, datumLength); + if(datumLength < sizeof(long)) + break; + } + else + result += string((char *) &datum, sizeof(long)); + } + + return result; + } + + std::string Debug::formatToAddress(unsigned long long address) + { + char addressString[17]; + + snprintf(addressString, 17, "%016llx", address); + + return string("0x") + addressString; + } + + void Debug::logPtraceSysCall(pid_t shellPid, uint16_t sysCallId, bool returnedFromSysCall, unsigned long long result, unsigned long long firstArgument, unsigned long long secondArgument, unsigned long long thirdArgument, unsigned long long fourthArgument, std::string fileName, int lineNo, std::string functionName) + { + const static uint8_t maxStringLength = 32; + string message = "syscall: "; + + string sysCallName; + if(sysCallId < MAX_NUMBER_OF_SYSCALLS) + sysCallName = syscalls[sysCallId]; + if(sysCallName.empty()) + sysCallName = "syscall" + std::to_string(sysCallId); + + + switch(sysCallId) + { + case SYS_read: + if(returnedFromSysCall) + message += (result == -1 ? "-1" : std::to_string((size_t) result)) + " = "; + message += sysCallName + + "(" + std::to_string((int) firstArgument) + + ", \"" + (returnedFromSysCall ? __debug_make_bytes_printable(peekData(shellPid, (char *) secondArgument, (size_t) min(thirdArgument, maxStringLength))) : "") + + (maxStringLength < thirdArgument || returnedFromSysCall == true ? "[…]" : "") + "\"" + + ", " + std::to_string((size_t) thirdArgument) + + ")"; + break; + case SYS_write: + if(returnedFromSysCall) + message += (result == -1 ? "-1" : std::to_string((size_t) result)) + " = "; + message += sysCallName + + "(" + std::to_string((int) firstArgument) + + ", \"" + __debug_make_bytes_printable(peekData(shellPid, (char *) secondArgument, (size_t) min(thirdArgument, maxStringLength))) + + (maxStringLength < thirdArgument || returnedFromSysCall == true ? "[…]" : "") + "\"" + + ", " + std::to_string((size_t) thirdArgument) + + ")"; + break; + case SYS_close: + if(returnedFromSysCall) + message += std::to_string((int) result) + " = "; + message += sysCallName + + "(" + std::to_string((int) firstArgument) + + ")"; + break; + case SYS_stat: + if(returnedFromSysCall) + message += std::to_string((int) result) + " = "; + message += sysCallName + + "(\"" + peekData(shellPid, (char *) firstArgument, -1) + "\"" + + ", " + formatToAddress(secondArgument) + + ")"; + break; + case SYS_fstat: + if(returnedFromSysCall) + message += std::to_string((int) result) + " = "; + message += sysCallName + + "(" + std::to_string((int) firstArgument) + + ", " + formatToAddress(secondArgument) + + ")"; + break; + case SYS_openat: + if(returnedFromSysCall) + message += std::to_string((int) result) + " = "; + message += sysCallName + + "(" + ((int) firstArgument == AT_FDCWD ? string("AT_FDCWD") : std::to_string((int) firstArgument)) + + ", \"" + __debug_make_bytes_printable(peekData(shellPid, (char *) secondArgument, -1)) + "\"" + + ", " + std::to_string((int) thirdArgument) + + ")"; + break; + case SYS_rt_sigprocmask: + if(returnedFromSysCall) + message += std::to_string((int) result) + " = "; + message += sysCallName + + "(" + std::to_string((int) firstArgument) + + ", " + formatToAddress(secondArgument) + + ", " + formatToAddress(thirdArgument) + + ", " + std::to_string((size_t) fourthArgument) + + ")"; + break; + case SYS_access: + if(returnedFromSysCall) + message += std::to_string((int) result) + " = "; + message += sysCallName + + "(" + peekData(shellPid, (char *) firstArgument, -1) + + ", " + std::to_string((int) secondArgument) + + ")"; + break; + case SYS_dup2: + if(returnedFromSysCall) + message += std::to_string((int) result) + " = "; + message += sysCallName + + "(" + std::to_string((int) firstArgument) + + ", " + std::to_string((int) secondArgument) + + ")"; + break; + case SYS_fcntl: + if(returnedFromSysCall) + message += std::to_string((int) result) + " = "; + message += sysCallName + + "(" + std::to_string((int) firstArgument) + + ", " + (secondArgument == 0 ? string("F_DUP_FD") : std::to_string((int) secondArgument)) + + ", …" + + ")"; + break; +/* case SYS_fstat: + struct stat statbuf; + message += "(" + std::to_string(firstArgument) + + ", \"" + __debug_make_bytes_printable(peekData(shellPid, (char *) secondArgument, (size_t) min(thirdArgument, maxStringLength))) + + (maxStringLength < thirdArgument ? "[…]" : "") + "\"" + + ", " + std::to_string((size_t) thirdArgument) + + ")"; + break; +*/ default: + if(returnedFromSysCall) + message += (result == -1 ? "-1" : std::to_string(result)) + " = "; + message += sysCallName + + "(" + formatToAddress(firstArgument) + + ", " + formatToAddress(secondArgument) + + ", " + formatToAddress(thirdArgument) + + ", " + formatToAddress(fourthArgument) + + ")"; + } + // source: https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ + + log(message, fileName, lineNo, functionName); + } + + 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..56d8281 --- /dev/null +++ b/Debug.hpp @@ -0,0 +1,132 @@ +/** + * @brief Writes messages to `debug.log` if `NDEBUG` is not defined. + * @author Christian Burger (christian@krikkel.de) + * @todo refactor macros + */ + +#ifndef __DEBUG_H__ +#define __DEBUG_H__ + +#ifndef NDEBUG + +#include +#include +#include +#include + +namespace krikkel +{ + class Debug + { + private: + std::fstream logFile; + static const int MAX_NUMBER_OF_SYSCALLS = 1024; + static const char * syscalls[MAX_NUMBER_OF_SYSCALLS]; + + Debug(); + std::string getTimestamp(); + std::string getUname(); + std::string peekData(pid_t shellPid, void *data, size_t length); + std::string formatToAddress(unsigned long long address); + + public: + void log(std::string message, std::string fileName, int lineNo + , std::string functionName); + void logPtraceSysCall(pid_t shellPid, uint16_t sysCallId, bool returnedFromSysCall, unsigned long long result, unsigned long long firstArgument, unsigned long long secondArgument, unsigned long long thirdArgument, unsigned long long fourthArgument, std::string fileName, int lineNo, std::string functionName); + static Debug *getInstance(); + }; +} + +#define __debug_log(message) \ + krikkel::Debug::getInstance()->log(message, __FILE__, __LINE__, __func__) + +#define __debug_log_ptrace_syscall(shellPid, sysCallId, returnedFromSysCall, result, firstArgument, secondArgument, thirdArgument, fourthArgument) \ + krikkel::Debug::getInstance()->logPtraceSysCall(shellPid, sysCallId, returnedFromSysCall, result, firstArgument, secondArgument, thirdArgument, fourthArgument, __FILE__, __LINE__, __func__); + +/// @todo need an explanation on why and how to use this +/// this is the complete opposite of self-explanatory +#define __debug_stringify_switch_begin(closureName, __prop, __value) \ + auto __debug_stringify__##closureName = [__prop, __value]() -> std::string \ + { \ + std::string propertyName, valueString; \ + switch(__prop) \ + { +#define __debug_stringify_switch_case(caseTestValue) \ + case caseTestValue: \ + propertyName = (propertyName.empty() ? #caseTestValue : propertyName) +#define __debug_stringify_switch_case_end_bool(value) \ + valueString = (value ? "true" : "false"); \ + break +#define __debug_stringify_switch_case_end_string(value) \ + valueString = "\"" + std::string(value) + "\""; \ + break +#define __debug_stringify_switch_case_end_number(value) \ + valueString = std::to_string(value); \ + break; +#define __debug_stringify_switch_end(__prop) \ + default: \ + propertyName = "default case triggerd for " + std::to_string(__prop); \ + break; \ + } \ + return propertyName + " = " + valueString; \ + } +#define __debug_stringify_get_string(closureName) __debug_stringify__##closureName() + +#define __debug_make_bytes_printable_table(__bytes) ([](std::string bytes) -> std::string \ + { \ + std::locale loc; \ + std::string result = "\n"; \ + int index; \ + for(index = 0; index < bytes.length(); ++index) \ + { \ + static const uint8_t TRANSLATION_BUFFER_SIZE = 4; \ + char translationBuffer[TRANSLATION_BUFFER_SIZE]; \ + if(std::isprint(bytes[index], loc)) \ + { \ + translationBuffer[0] = translationBuffer[1] = ' '; \ + translationBuffer[2] = bytes[index]; \ + translationBuffer[3] = '\0'; \ + } \ + else \ + std::snprintf(translationBuffer, TRANSLATION_BUFFER_SIZE, " %02x", bytes[index]); \ + result += (index % 16 == 0 ? " | " : " "); \ + result += translationBuffer; \ + if(index % 16 == 15) \ + result += '\n'; \ + } \ + if(index % 16) \ + result += '\n'; \ + return result; \ + } \ + )(__bytes) + +#define __debug_make_bytes_printable(__bytes) ([](std::string bytes) -> std::string \ + { \ + std::locale loc; \ + std::string result; \ + for(char character : bytes) \ + { \ + if(std::isprint(character, loc) && character != '<') \ + result += character; \ + else \ + result += '<' + std::to_string((unsigned char) character) + '>'; \ + } \ + return result; \ + })(__bytes) + +#else + +/// @todo check what's the business with this "((void)0)" instead of empty macro +#define __debug_log(message) +#define __debug_stringify_switch_begin(closureName, __prop, __value) +#define __debug_stringify_switch_case(caseTestValue) +#define __debug_stringify_switch_case_end_bool(value) +#define __debug_stringify_switch_case_end_string(value) +#define __debug_stringify_switch_case_end_number(value) +#define __debug_stringify_switch_end(__prop) +#define __debug_make_bytes_printable_table(__bytes) +#define __debug_make_bytes_printable(__bytes) + +#endif /* NDEBUG */ + +#endif // __DEBUG_H__ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cc1f711 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Christian Burger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..49bec5e --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +## Description + +**WARNING**: This is a prototype. Things will probably break; in spectacular +ways. + +`NCursesPtyWindow` provides a pseudo terminal in a ncurses window. + +## Building + +Requires: + +* GCC 9.3 (C++17) +* ncurses 6.2 +* libvterm (0.1.2-2; r740 at https://bazaar.launchpad.net/~libvterm/libvterm/trunk/revision/740) +* libmsgsl-dev 2.1.0-1 (Microsoft C++ Guidelines Support Library) + +## Running + +Though this is a library, there is a demo application. It starts the currently +running shell (or `/bin/bash` if `SHELL` environment variable is not set) and +runs it in a ncurses window. \ No newline at end of file diff --git a/SingleUserInput.cpp b/SingleUserInput.cpp new file mode 100644 index 0000000..ec01285 --- /dev/null +++ b/SingleUserInput.cpp @@ -0,0 +1,105 @@ +/** + * @author Christian Burger (christian@krikkel.de) + */ + +#include +#include "Debug.hpp" + +#include + +#define KEY_TAB '\t' +#define KEY_ESCAPE 27 +#define KEY_RETURN 10 +#define KEY_ALT_BACKSPACE 127 +#define KEY_ALT_ALT_BACKSPACE '\b' + +namespace krikkel::NCursesPtyWindow +{ + SingleUserInput::SingleUserInput(int _resultGetWchCall, wint_t _input) + : input(_input), resultGetWchCall(_resultGetWchCall) + { + + } + + bool SingleUserInput::isNormalKey() + { + return resultGetWchCall == OK; + } + + bool SingleUserInput::isFunctionKey() + { + return resultGetWchCall == KEY_CODE_YES; + } + + VTermKey SingleUserInput::mapKeyNcursesToVTerm() + { + if(input >= KEY_MAX) + return VTERM_KEY_NONE; + + debug(); + + // thought about array mapping instead of `switch()` statements, but as + // it is only processing user input, it is not time critical enough for + // the wasted space + + // @tode unmapped keys: keys erase + if(resultGetWchCall == OK) + switch(input) + { + case KEY_TAB: + return VTERM_KEY_TAB; + case KEY_ESCAPE: + return VTERM_KEY_ESCAPE; + default: + ; // we cannot translate + } + + if(resultGetWchCall == KEY_CODE_YES) + switch(input) + { + case KEY_ENTER: + return VTERM_KEY_ENTER; + case KEY_BACKSPACE: + return VTERM_KEY_BACKSPACE; + case KEY_UP: + return VTERM_KEY_UP; + case KEY_DOWN: + return VTERM_KEY_DOWN; + case KEY_LEFT: + return VTERM_KEY_LEFT; + case KEY_RIGHT: + return VTERM_KEY_RIGHT; + case KEY_IC: + return VTERM_KEY_INS; + case KEY_DC: + return VTERM_KEY_DEL; + case KEY_HOME: + return VTERM_KEY_HOME; + case KEY_END: + return VTERM_KEY_END; + case KEY_PPAGE: + return VTERM_KEY_PAGEUP; + case KEY_NPAGE: + return VTERM_KEY_PAGEDOWN; + default: + if(input >= KEY_F0 && input < KEY_F0 + 12) + return static_cast(VTERM_KEY_FUNCTION(input - KEY_F0)); + } + __debug_log("previous ncurses input could not be decoded to vterm input"); + return VTERM_KEY_NONE; + } + + wint_t SingleUserInput::getRawInput() + { + return input; + } + + void SingleUserInput::debug() + { +#ifndef NDEGUG + char octalRepresentation[16]; + snprintf(octalRepresentation, 16, "%o", input); + __debug_log("mapping ncurses key: " + std::to_string(input) + " octal: " + octalRepresentation); +#endif // NDEBUG + } +} \ No newline at end of file diff --git a/Window.cpp b/Window.cpp new file mode 100644 index 0000000..13ea035 --- /dev/null +++ b/Window.cpp @@ -0,0 +1,345 @@ +/** + * @author Christian Burger (christian@krikkel.de) + */ + +#include +#include "Debug.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace krikkel::NCursesPtyWindow +{ + using gsl::narrow; + using std::lock_guard; + + Window::Window(std::mutex *writeToNCursesMutex, int lines, int columns, int y, int x) + : writeToNCursesMutex(writeToNCursesMutex), NCursesWindow(lines, columns, y, x) + { + // to get the original terminal we need to shutdown ncurses for a moment + /// @todo maybe try `reset_prog_mode()` and `reset_shell_mode()` instead + ::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); + vterm_set_utf8(pseudoTerminal, true); + pseudoTerminalScreen = vterm_obtain_screen(pseudoTerminal); + vterm_screen_reset(pseudoTerminalScreen, true); + vterm_screen_set_callbacks(pseudoTerminalScreen, &screenCallbacks, this); + vterm_output_set_callback(pseudoTerminal, &staticHandlerOutput, this); + vterm_screen_enable_altscreen(pseudoTerminalScreen, true); + + //raw(); //— cbreak might suffice + //noecho(); — already set + //nodelay(true); — @todo needs some reprogramming + keypad(true); + nonl(); + + /// @todo block all signals, this thread does not handle any + readPtyClientThread = + std::thread(&Window::readFromPtyClientThreadMethod, this); + } + + Window::~Window() + { + close(fdPtyHost); + close(fdPtyClient); + vterm_free(pseudoTerminal); + } + + int Window::getFdPtyClient() const + { + return fdPtyClient; + } + + void Window::writeToClient(const char *output, size_t length) + { + lock_guard writeLock(writeToPseudoTerminalMutex); + write(fdPtyHost, output, length); + __debug_log("written to PTY client: '" + __debug_make_bytes_printable(std::string(output, length)) + '\''); + } + + void Window::writeUnicodeCharToClient(wint_t character) + { + vterm_keyboard_unichar(pseudoTerminal, character, VTERM_MOD_NONE); + } + + void Window::writeKeyToClient(VTermKey key) + { + __debug_log("writing key: " + std::to_string(key)); + vterm_keyboard_key(pseudoTerminal, key, VTERM_MOD_NONE); + } + + SingleUserInput Window::readSingleUserInput() + { + wint_t input; + int result = get_wch(&input); + return SingleUserInput(result, input); + } + + void Window::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 Window::readFromPtyClient() + { + size_t bytesRead = read(fdPtyHost, ptyClientOutputBuffer, PTY_CLIENT_OUTPUT_BUFFER_SIZE); + if(bytesRead != -1 && bytesRead != 0) + { + lock_guard writeLock(writeToPseudoTerminalMutex); + vterm_input_write(pseudoTerminal, ptyClientOutputBuffer, bytesRead); + } + __debug_log("read from PTY client: '" + __debug_make_bytes_printable(std::string(ptyClientOutputBuffer, bytesRead)) + '\''); + } + + VTermScreenCallbacks Window::screenCallbacks = + { + .damage = staticHandlerDamage, + .moverect = staticHandlerMoveRect, + .movecursor = staticHandlerMoveCursor, + .settermprop = staticHandlerSetTermProp, + .bell = staticHandlerBell, + .resize = staticHandlerResize, + .sb_pushline = staticHandlerPushLine, + .sb_popline = staticHandlerPopLine, + }; + + int Window::handlerDamage(VTermRect rect) + { + 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); + + refresh(); + + return 0; + } + + int Window::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 Window::handlerMoveCursor(VTermPos pos, VTermPos oldpos, int visible) + { + /// @todo maybe use `mvcur()` instead? + cursorX = pos.col; + cursorY = pos.row; + refresh(); + return 0; + } + + int Window::handlerSetTermProp(VTermProp prop, VTermValue *val) + { + /// @todo maybe use "vterm_get_prop_type() —> bool, number, string"? + __debug_stringify_switch_begin(handlerSetTermProp, prop, val); + __debug_stringify_switch_case(VTERM_PROP_CURSORVISIBLE); + __debug_stringify_switch_case(VTERM_PROP_CURSORBLINK); + __debug_stringify_switch_case(VTERM_PROP_ALTSCREEN); + __debug_stringify_switch_case(VTERM_PROP_REVERSE); + __debug_stringify_switch_case_end_bool(val->boolean); + __debug_stringify_switch_case(VTERM_PROP_TITLE); + __debug_stringify_switch_case(VTERM_PROP_ICONNAME); + __debug_stringify_switch_case_end_string(val->string); + __debug_stringify_switch_case(VTERM_PROP_CURSORSHAPE); + __debug_stringify_switch_case(VTERM_PROP_MOUSE); + __debug_stringify_switch_case_end_number(val->number); + __debug_stringify_switch_end(prop); + + __debug_log(std::string("unimplemented handler called: ") + + __debug_stringify_get_string(handlerSetTermProp) + ); + return 0; + } + + int Window::handlerBell() + { + beep(); + return 0; + } + + int Window::handlerResize(int rows, int cols) + { + __debug_log("unimplemented handler called"); + return 0; + } + + int Window::handlerPushLine(int cols, const VTermScreenCell *cells) + { + __debug_log("(unimplemented) push line with " + std::to_string(cols) + " columns"); + return 0; + } + + int Window::handlerPopLine(int cols, VTermScreenCell *cells) + { + __debug_log("unimplemented handler called"); + return 0; + } + + attr_t Window::extractAttributesFromVTermCell(VTermScreenCell vTermCell) + { + attr_t result = A_NORMAL; + //__debug_log("unimplemented method called"); + return result; + } + + short Window::extractColorFromVTermCell(VTermScreenCell vTermCell) + { + //__debug_log("unimplemented method called"); + return 0; + } + + void Window::copyPtyCellToNCursesWindow(int x, int y) + { + VTermPos cellPosition = {y, x}; + VTermScreenCell vTermCell; + cchar_t converted; + attr_t formatting; + short colorPair; + wchar_t character; + + vterm_screen_get_cell(pseudoTerminalScreen, cellPosition, &vTermCell); + formatting = extractAttributesFromVTermCell(vTermCell); + colorPair = extractColorFromVTermCell(vTermCell); + + if(vTermCell.chars[0] == 0) + character = ' '; + else + character = *vTermCell.chars; + + { + lock_guard nCursesLock(*writeToNCursesMutex); + setcchar(&converted, &character, formatting, colorPair, NULL); + move(cellPosition.row, cellPosition.col); + chgat(1, formatting, colorPair, NULL); + add_wch(&converted); + } + } + + int Window::add_wch(const cchar_t *character) + { + return ::wadd_wch(w, character); + } + + int Window::get_wch(wint_t *character) + { + return ::wget_wch(w, character); + } + + int Window::refresh() + { + lock_guard nCursesLock(*writeToNCursesMutex); + move(cursorY, cursorX); + return NCursesWindow::refresh(); + } + + /// @todo potential racing condition where drawing into terminal while + /// resizing? + int Window::wresize(int rows, int cols) + { + lock_guard nCursesLock(*writeToNCursesMutex); + lock_guard writeLock(writeToPseudoTerminalMutex); + winsize windowSize = + { + .ws_row = narrow(rows) + , .ws_col = narrow(cols) + }; + ioctl(fdPtyHost, TIOCSWINSZ, &windowSize); + vterm_set_size(pseudoTerminal, rows, cols); + + return NCursesWindow::wresize(rows, cols); + } + + int Window::staticHandlerDamage(VTermRect rect, void *user) + { + Window *instance = static_cast(user); + return instance->handlerDamage(rect); + } + + int Window::staticHandlerMoveRect(VTermRect dest, VTermRect src, void *user) + { + Window *instance = static_cast(user); + return instance->handlerMoveRect(dest, src); + } + + int Window::staticHandlerMoveCursor(VTermPos pos, VTermPos oldpos, int visible, void *user) + { + Window *instance = static_cast(user); + return instance->handlerMoveCursor(pos, oldpos, visible); + } + + int Window::staticHandlerSetTermProp(VTermProp prop, VTermValue *val, void *user) + { + Window *instance = static_cast(user); + return instance->handlerSetTermProp(prop, val); + } + + int Window::staticHandlerBell(void *user) + { + Window *instance = static_cast(user); + return instance->handlerBell(); + } + + int Window::staticHandlerResize(int rows, int cols, void *user) + { + Window *instance = static_cast(user); + return instance->handlerResize(rows, cols); + } + + int Window::staticHandlerPushLine(int cols, const VTermScreenCell *cells, void *user) + { + Window *instance = static_cast(user); + return instance->handlerPushLine(cols, cells); + } + + int Window::staticHandlerPopLine(int cols, VTermScreenCell *cells, void *user) + { + Window *instance = static_cast(user); + return instance->handlerPopLine(cols, cells); + } + + void Window::staticHandlerOutput(const char *s, size_t len, void *user) + { + Window *instance = static_cast(user); + __debug_log("output handler writing to client: '" + __debug_make_bytes_printable(std::string(s, len)) + "'"); + instance->writeToClient(s, len); + } +} \ No newline at end of file diff --git a/cmake/gsl.cmake b/cmake/gsl.cmake new file mode 100644 index 0000000..dabc374 --- /dev/null +++ b/cmake/gsl.cmake @@ -0,0 +1,11 @@ +## +# @brief Includes Microsoft's "C++ Guidelines Standard Library" +# @author Christian Burger + +find_path(GSL_INCLUDE_DIR "gsl/gsl" REQUIRED) +if(NOT GSL_INCLUDE_DIR) + message(SEND_ERROR "Microsoft GSL not found.") +else() + message(STATUS "Found Microsoft C++ GSL.") + include_directories(SYSTEM ${GSL_INCLUDE_DIR}) +endif() \ No newline at end of file diff --git a/cmake/libvterm.cmake b/cmake/libvterm.cmake new file mode 100644 index 0000000..10cf61d --- /dev/null +++ b/cmake/libvterm.cmake @@ -0,0 +1,41 @@ +## +# @brief Includes `libvterm` (builds it if not found on the system). +# +# Provides ${LIBVTERM_LIBRARY} and implicitly adds path for correct +# `#include<>`. +# +# @warning dependency must be added like this: +# if(TARGET libvtermProject) +# add_dependencies( libvtermProject) +# endif() +# +# @author Christian Burger + + +find_library(LIBVTERM_LIBRARY NAMES "libvterm.so.0.0.2" "vterm") +find_path(LIBVTERM_INCLUDE "vterm.h") + +if(NOT LIBVTERM_LIBRARY) + message(STATUS "Did not find `libvterm.so`.") + set(LIBVTERM_BUILD_IT TRUE) +endif() +if(NOT LIBVTERM_INCLUDE) + message(STATUS "Did not find `vterm.h`.") + set(LIBVTERM_BUILD_IT TRUE) +endif() +if(LIBVTERM_BUILD_IT) + include(ExternalProject) + message(STATUS "Did not find libvterm (see previous error) — building it.") + ExternalProject_Add( + libvtermProject + DOWNLOAD_COMMAND URL "https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/libvterm/0.1.2-2/libvterm_0.1.2.orig.tar.gz" + CONFIGURE_COMMAND "" + BUILD_IN_SOURCE true + INSTALL_COMMAND "" + ) + ExternalProject_Get_property(libvtermProject SOURCE_DIR) + set(LIBVTERM_LIBRARY "${SOURCE_DIR}/.libs/libvterm.a") + include_directories(SYSTEM "${SOURCE_DIR}/include") +else() + message(STATUS "Found vterm library.") +endif() diff --git a/cmake/ncurses.cmake b/cmake/ncurses.cmake new file mode 100644 index 0000000..fbbbad7 --- /dev/null +++ b/cmake/ncurses.cmake @@ -0,0 +1,15 @@ +## +# @brief Includes the UTF-8 C++ version of the `ncurses` library. +# @author Christian Burger + +set(CURSES_NEED_NCURSES TRUE) +set(CURSES_NEED_WIDE TRUE) +find_package(Curses 6.2 REQUIRED) +include_directories(SYSTEM ${CURSES_INCLUDE_DIRS}) + +# find C++ interface for ncurses with unicode support +find_library(CURSES_CPP_WIDE_LIBRARY NAMES ncurses++w) +if(NOT CURSES_CPP_WIDE_LIBRARY) + message(SEND_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/cmake/version.cmake b/cmake/version.cmake new file mode 100644 index 0000000..2f1520b --- /dev/null +++ b/cmake/version.cmake @@ -0,0 +1,77 @@ +## +# @brief a ${SEMANTIC_VERSION} based on Git tags (or a `VERSION` file) à la +# `v..` is provided +# +# Versions have the format "v.." in accordance with +# Semantic Versioning. If a version is provided by a file named `VERSION`, the +# full version must be given. If a version is provided by Git tags, only part of +# the version must be given, the format is then: "v.". The +# version is automatically deduced from the distance in commits to the latest +# Git tag. The version set in the file overrides the tags in the Git repository. +# +# based upon Semantic Versioning: https://semver.org/ +# +# @attention The version-part is a bit flaky; it gives the distance in +# commits to the . version. Due to the non-linear +# nature of the commit history (because of branches), it is ambigous +# (i. e. multiple commits can be found at the same distance). On top +# of that, it is your job to make sure, that patches only fix bugs +# and do not change the API (no new functionalty; or old removed). +# +# One option to address both issues is: Stay in one branch for +# releases — for example named 'release' — and cherry-pick new +# versions into the branch (alphas, betas, candidates and releases). +# Optional: squash cherry-picked commits, so that the part of +# the version is only incremented by one. +# @warning To update the version you have to run CMake again. +# +# @todo escape/quote paths +# @todo test cases for CMake file components? +# @todo make variables local (see: unset()) and put them in a "namespace" +# @author Christian Burger +find_package(Git) + +function(get_semantic_version) + if(EXISTS "${CMAKE_SOURCE_DIR}/VERSION") + message(STATUS "using file `VERSION` to determine version") + file(READ "VERSION" REPOSITORY_VERSION) + elseif(GIT_FOUND) + message(STATUS "using Git to determine project version") + execute_process(COMMAND ${GIT_EXECUTABLE} -C ${CMAKE_SOURCE_DIR} describe --always --match v[0-9]*.[0-9]* + OUTPUT_VARIABLE REPOSITORY_VERSION) + else() + message(WARNING "We need Git or a file \"VERSION\" to determine a version (e. g. \"v1.0\").") + endif() + + # test cases + #set(REPOSITORY_VERSION "v0.1") # = 0.1.0 + #set(REPOSITORY_VERSION "v0.1-1-g12345678") # = 0.1.1 + #set(REPOSITORY_VERSION "v0.1") # = 0.0.0 + #set(REPOSITORY_VERSION "1234abcd") # = 0.0.0 + #set(REPOSITORY_VERSION "") # = 0.1.0 + + string(REGEX MATCH "^v([0-9]+)\.([0-9]+)([-\\.]([0-9]+))?" SEMANTIC_VERSION_PREPROCESSED "${REPOSITORY_VERSION}") + + if("${SEMANTIC_VERSION_PREPROCESSED}" STREQUAL "") + message(STATUS "Found no version tag (e. g. \"v1.0\").") + endif() + + set(SEMANTIC_VERSION_MAJOR ${CMAKE_MATCH_1}) + set(SEMANTIC_VERSION_MINOR ${CMAKE_MATCH_2}) + set(SEMANTIC_VERSION_PATCH ${CMAKE_MATCH_4}) + + if("${SEMANTIC_VERSION_MAJOR}" STREQUAL "") + set(SEMANTIC_VERSION "0.0.0") + else() + if("${SEMANTIC_VERSION_PATCH}" STREQUAL "") + set(SEMANTIC_VERSION_PATCH "0") + endif() + set(SEMANTIC_VERSION "${SEMANTIC_VERSION_MAJOR}.${SEMANTIC_VERSION_MINOR}.${SEMANTIC_VERSION_PATCH}") + endif() + + message(STATUS "Project version is: ${SEMANTIC_VERSION}") + + set(SEMANTIC_VERSION "${SEMANTIC_VERSION}" PARENT_SCOPE) +endfunction() + +get_semantic_version() diff --git a/include/NCursesPtyWindow/SingleUserInput.hpp b/include/NCursesPtyWindow/SingleUserInput.hpp new file mode 100644 index 0000000..7d21e5b --- /dev/null +++ b/include/NCursesPtyWindow/SingleUserInput.hpp @@ -0,0 +1,31 @@ +/** + * @brief stores a single user input (printable and function keys) + * @author Christian Burger (christian@krikkel.de) + */ + +#ifndef F0E30ED4_3883_40D6_A6EE_08BA4DF9E92E +#define F0E30ED4_3883_40D6_A6EE_08BA4DF9E92E + +#include +#include + +namespace krikkel::NCursesPtyWindow +{ + class SingleUserInput + { + public: + SingleUserInput(int resultGetWchCall, wint_t input); + + bool isNormalKey(); + bool isFunctionKey(); + VTermKey mapKeyNcursesToVTerm(); + wint_t getRawInput(); + + private: + wint_t input; + int resultGetWchCall; + void debug(); + }; +} + +#endif /* F0E30ED4_3883_40D6_A6EE_08BA4DF9E92E */ diff --git a/include/NCursesPtyWindow/Window.hpp b/include/NCursesPtyWindow/Window.hpp new file mode 100644 index 0000000..6aaa542 --- /dev/null +++ b/include/NCursesPtyWindow/Window.hpp @@ -0,0 +1,91 @@ +/** + * @brief `ncurses` window displaying contents of a pseudo terminal + * @author Christian Burger (christian@krikkel.de) + */ + +#ifndef __WINDOW_H__ +#define __WINDOW_H__ + +#include "SingleUserInput.hpp" + +#include +#include +#include +#include +#include +#include + +#ifdef add_wch +inline void UNDEF(add_wch)(const cchar_t *character) { add_wch(character); } +#undef add_wch +#define add_wch UNDEF(add_wch) +#endif + +#ifdef get_wch +inline void UNDEF(get_wch)(wint_t *character) { get_wch(character); } +#undef get_wch +#define get_wch UNDEF(get_wch) +#endif + +namespace krikkel::NCursesPtyWindow +{ + class Window : public NCursesWindow + { + public: + Window(std::mutex *writeToNCursesMutex, int lines, int columns, int y, int x); + ~Window(); + + int getFdPtyClient() const; + void writeToClient(const char * string, size_t length); + void writeUnicodeCharToClient(wint_t character); + void writeKeyToClient(VTermKey key); + + SingleUserInput readSingleUserInput(); + + int add_wch(const cchar_t *character); + int get_wch(wint_t *character); + int refresh() override; + int wresize(int rows, int cols); + + private: + int fdPtyHost, fdPtyClient; + struct termios terminalParameters; + VTerm *pseudoTerminal; + std::mutex writeToPseudoTerminalMutex; + std::mutex *writeToNCursesMutex; + VTermScreen *pseudoTerminalScreen; + static VTermScreenCallbacks screenCallbacks; + /// @todo one line is at most 4096 chars long + static const uint16_t PTY_CLIENT_OUTPUT_BUFFER_SIZE = 2 * 4096; + char ptyClientOutputBuffer[PTY_CLIENT_OUTPUT_BUFFER_SIZE]; + uint16_t cursorX, cursorY; + + 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); + + attr_t extractAttributesFromVTermCell(VTermScreenCell cell); + short extractColorFromVTermCell(VTermScreenCell cell); + 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); + static void staticHandlerOutput(const char *s, size_t len, void *user); + }; +} +#endif // __WINDOW_H__ \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..3675a16 --- /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 "App.hpp" + +krikkel::NCursesPtyWindow::App cursesPtyApp; \ No newline at end of file