commit 2863c8ae16a3002fe760e6d4c0daad46f0d3d164 Author: Christian Burger Date: Sat Apr 23 22:56:47 2022 +0200 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. 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