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.
This commit is contained in:
Christian Burger 2022-04-23 22:56:47 +02:00
commit 2863c8ae16
16 changed files with 1550 additions and 0 deletions

63
App.cpp Normal file
View file

@ -0,0 +1,63 @@
/**
* @author Christian Burger (christian@krikkel.de)
*/
#include "App.hpp"
#include "Debug.hpp"
#include <NCursesPtyWindow/Window.hpp>
#include <unistd.h>
#include <utmp.h>
#include <cstdlib>
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, "<CTRL>+C exits shell.\n\n");
execv(shellPath, const_cast<char * const *>(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;
}
}

23
App.hpp Normal file
View file

@ -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 <cursesapp.h>
namespace krikkel::NCursesPtyWindow
{
class App : public NCursesApplication
{
public:
App();
private:
int run() override;
};
}
#endif /* A3B2AE4E_0A39_468C_8CCA_E6508166702A */

62
CMakeLists.txt Normal file
View file

@ -0,0 +1,62 @@
##
# @author Christian Burger <christian@krikkel.de>
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 <christian@krikkel.de>")
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)

503
Debug.cpp Normal file
View file

@ -0,0 +1,503 @@
/**
* @author Christian Burger (christian@krikkel.de)
* @todo Switch over to <seccomp.h>? 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 <algorithm>
#include <cstring>
#include <filesystem>
#include <cstdio>
#include <syscall.h>
#include <sys/ptrace.h>
#include <sys/stat.h>
#include <fcntl.h>
#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<unsigned long long>(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<unsigned long long>(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<unsigned long long>(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 <sys/syscall.h>" }
/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
*/

132
Debug.hpp Normal file
View file

@ -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 <fstream>
#include <string>
#include <ctime>
#include <locale>
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__

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Christian Burger <christian@krikkel.de>
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.

21
README.md Normal file
View file

@ -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.

105
SingleUserInput.cpp Normal file
View file

@ -0,0 +1,105 @@
/**
* @author Christian Burger (christian@krikkel.de)
*/
#include <NCursesPtyWindow/SingleUserInput.hpp>
#include "Debug.hpp"
#include <vterm.h>
#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<VTermKey>(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
}
}

345
Window.cpp Normal file
View file

@ -0,0 +1,345 @@
/**
* @author Christian Burger (christian@krikkel.de)
*/
#include <NCursesPtyWindow/Window.hpp>
#include "Debug.hpp"
#include <cstdio>
#include <unistd.h>
#include <sys/select.h>
#include <gsl/gsl>
#include <cctype>
#include <termios.h>
#include <fcntl.h>
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<short unsigned int>(this->height())
, .ws_col = narrow<short unsigned int>(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<unsigned short>(rows)
, .ws_col = narrow<unsigned short>(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<Window *>(user);
return instance->handlerDamage(rect);
}
int Window::staticHandlerMoveRect(VTermRect dest, VTermRect src, void *user)
{
Window *instance = static_cast<Window *>(user);
return instance->handlerMoveRect(dest, src);
}
int Window::staticHandlerMoveCursor(VTermPos pos, VTermPos oldpos, int visible, void *user)
{
Window *instance = static_cast<Window *>(user);
return instance->handlerMoveCursor(pos, oldpos, visible);
}
int Window::staticHandlerSetTermProp(VTermProp prop, VTermValue *val, void *user)
{
Window *instance = static_cast<Window *>(user);
return instance->handlerSetTermProp(prop, val);
}
int Window::staticHandlerBell(void *user)
{
Window *instance = static_cast<Window *>(user);
return instance->handlerBell();
}
int Window::staticHandlerResize(int rows, int cols, void *user)
{
Window *instance = static_cast<Window *>(user);
return instance->handlerResize(rows, cols);
}
int Window::staticHandlerPushLine(int cols, const VTermScreenCell *cells, void *user)
{
Window *instance = static_cast<Window *>(user);
return instance->handlerPushLine(cols, cells);
}
int Window::staticHandlerPopLine(int cols, VTermScreenCell *cells, void *user)
{
Window *instance = static_cast<Window *>(user);
return instance->handlerPopLine(cols, cells);
}
void Window::staticHandlerOutput(const char *s, size_t len, void *user)
{
Window *instance = static_cast<Window *>(user);
__debug_log("output handler writing to client: '" + __debug_make_bytes_printable(std::string(s, len)) + "'");
instance->writeToClient(s, len);
}
}

11
cmake/gsl.cmake Normal file
View file

@ -0,0 +1,11 @@
##
# @brief Includes Microsoft's "C++ Guidelines Standard Library"
# @author Christian Burger <christian@krikkel.de>
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()

41
cmake/libvterm.cmake Normal file
View file

@ -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(<targetDependingOn> libvtermProject)
# endif()
#
# @author Christian Burger <christian@krikkel.de>
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()

15
cmake/ncurses.cmake Normal file
View file

@ -0,0 +1,15 @@
##
# @brief Includes the UTF-8 C++ version of the `ncurses` library.
# @author Christian Burger <christian@krikkel.de>
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})

77
cmake/version.cmake Normal file
View file

@ -0,0 +1,77 @@
##
# @brief a ${SEMANTIC_VERSION} based on Git tags (or a `VERSION` file) à la
# `v<major>.<minor>.<patch>` is provided
#
# Versions have the format "v<major>.<minor>.<patch>" 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<major>.<minor>". The <patch>
# 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 <patch> version-part is a bit flaky; it gives the distance in
# commits to the <major>.<minor> 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 <patch> 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 <christian@krikkel.de>
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()

View file

@ -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 <cursesw.h>
#include <vterm_keycodes.h>
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 */

View file

@ -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 <cursesw.h>
#include <pty.h>
#include <vterm.h>
#include <string>
#include <thread>
#include <mutex>
#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__

9
main.cpp Normal file
View file

@ -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;