diff --git a/.clang-format b/.clang-format index 336112f..6142558 100644 --- a/.clang-format +++ b/.clang-format @@ -21,6 +21,7 @@ BinPackParameters: true BreakConstructorInitializers: BeforeColon BreakStringLiterals: false +ColumnLimit: 0 CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: true diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d553d38 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.15) + +project(nanosm + LANGUAGES CXX +) + +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") + +include(Policies) +include(ProjectFuncs) +include(CompilerFlags) + + +add_executable(nanosm + src/main.cpp + + src/WordExp.cpp + + src/EventLoop.cpp +) + +nanosm_target(nanosm) + +# todo: cmake install sex diff --git a/cmake/CompilerFlags-GNU.cmake b/cmake/CompilerFlags-GNU.cmake new file mode 100644 index 0000000..b67a82a --- /dev/null +++ b/cmake/CompilerFlags-GNU.cmake @@ -0,0 +1,67 @@ +# TODO: This currently assumes libstdc++, later on we should *probably* set this with some detection +# also TODO: Use a list so that this isn't one giant line (list JOIN should help.) +set(NANOSM_CORE_COMPILE_ARGS "-Wall -Wformat=2 -Wimplicit-fallthrough -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -fstrict-flex-arrays=3 -fstack-clash-protection -fstack-protector-strong") +set(NANOSM_CORE_LINKER_ARGS "-fuse-ld=${NANOSM_LINKER}") + +if("${CMAKE_BUILD_TYPE}" STREQUAL "Release") # OR "${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo" + # If on Release use link-time optimizations. + # On clang we use ThinLTO for even better build performance. + if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + set(NANOSM_CORE_COMPILE_ARGS "${NANOSM_CORE_COMPILE_ARGS} -flto=thin") + set(NANOSM_CORE_LINKER_ARGS "${NANOSM_CORE_LINKER_ARGS} -flto=thin") + else() + set(NANOSM_CORE_COMPILE_ARGS "${NANOSM_CORE_COMPILE_ARGS} -flto") + set(NANOSM_CORE_LINKER_ARGS "${NANOSM_CORE_LINKER_ARGS} -flto") + endif() +endif() + +set(_NANOSM_CORE_WANTED_SANITIZERS "") + +if("asan" IN_LIST NANOSM_SANITIZERS) + # Error if someone's trying to mix asan and tsan together since they aren't compatible. + if("tsan" IN_LIST NANOSM_SANITIZERS) + message(FATAL_ERROR "ASAN and TSAN cannot be used together.") + endif() + + message(STATUS "Enabling ASAN because it was in NANOSM_SANITIZERS") + list(APPEND _NANOSM_CORE_WANTED_SANITIZERS "address") +endif() + +if("tsan" IN_LIST NANOSM_SANITIZERS) + if("asan" IN_LIST NANOSM_SANITIZERS) + message(FATAL_ERROR "ASAN and TSAN cannot be used together.") + endif() + + message(STATUS "Enabling TSAN because it was in NANOSM_SANITIZERS") + list(APPEND _NANOSM_CORE_WANTED_SANITIZERS "thread") +endif() + +if("ubsan" IN_LIST NANOSM_SANITIZERS) + message(STATUS "Enabling UBSAN because it was in NANOSM_SANITIZERS") + list(APPEND _NANOSM_CORE_WANTED_SANITIZERS "undefined") +endif() + +list(LENGTH _NANOSM_CORE_WANTED_SANITIZERS _NANOSM_CORE_WANTED_SANITIZERS_LENGTH) +if(NOT _NANOSM_CORE_WANTED_SANITIZERS_LENGTH EQUAL 0) + list(JOIN _NANOSM_CORE_WANTED_SANITIZERS "," _NANOSM_CORE_WANTED_SANITIZERS_ARG) + message(STATUS "Enabled sanitizers: ${_NANOSM_CORE_WANTED_SANITIZERS_ARG}") + set(NANOSM_CORE_COMPILE_ARGS "${NANOSM_CORE_COMPILE_ARGS} -fsanitize=${_NANOSM_CORE_WANTED_SANITIZERS_ARG}") + set(NANOSM_CORE_LINKER_ARGS "${NANOSM_CORE_LINKER_ARGS} -fsanitize=${_NANOSM_CORE_WANTED_SANITIZERS_ARG}") +endif() + +# Set core CMake toolchain variables so that they get applied to all projects. +# A bit nasty, but /shrug, this way our third party libraries can be mostly sanitized/etc as well. +# We do NOT do this for CMake compiler features however. + +set(CMAKE_C_FLAGS "${NANOSM_CORE_COMPILE_ARGS}") +set(CMAKE_CXX_FLAGS "${NANOSM_CORE_COMPILE_ARGS}") + +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS} -O0 -g3") +set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS} -O3 -g3") +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS} -O3") + +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -O0 -g3") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS} -O3 -g3") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -O3") + +set(CMAKE_EXE_LINKER_FLAGS "${NANOSM_CORE_LINKER_ARGS} -Wl,-z,noexecstack,-z,relro,-z,now") diff --git a/cmake/CompilerFlags.cmake b/cmake/CompilerFlags.cmake new file mode 100644 index 0000000..1f382fd --- /dev/null +++ b/cmake/CompilerFlags.cmake @@ -0,0 +1,9 @@ +# Core compile arguments used for the whole project +# +# This is the driver, we include compiler/platform specific files here + +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + include(CompilerFlags-GNU) +else() + message(FATAL_ERROR "Unsupported (for now?) compiler ${CMAKE_CXX_COMPILER_ID}") +endif() diff --git a/cmake/Policies.cmake b/cmake/Policies.cmake new file mode 100644 index 0000000..d7a084b --- /dev/null +++ b/cmake/Policies.cmake @@ -0,0 +1,22 @@ +# CMake policy configuration + +# Macro to enable new CMake policy. +# Makes this file a *LOT* shorter. +macro (_new_cmake_policy policy) + if(POLICY ${policy}) + #message(STATUS "Enabling new policy ${policy}") + cmake_policy(SET ${policy} NEW) + endif() +endmacro() + +_new_cmake_policy(CMP0026) # CMake 3.0: Disallow use of the LOCATION property for build targets. +_new_cmake_policy(CMP0042) # CMake 3.0+ (2.8.12): MacOS "@rpath" in target's install name +_new_cmake_policy(CMP0046) # warn about non-existent dependencies +_new_cmake_policy(CMP0048) # CMake 3.0+: project() command now maintains VERSION +_new_cmake_policy(CMP0054) # CMake 3.1: Only interpret if() arguments as variables or keywords when unquoted. +_new_cmake_policy(CMP0056) # try_compile() linker flags +_new_cmake_policy(CMP0066) # CMake 3.7: try_compile(): use per-config flags, like CMAKE_CXX_FLAGS_RELEASE +_new_cmake_policy(CMP0067) # CMake 3.8: try_compile(): honor language standard variables (like C++11) +_new_cmake_policy(CMP0068) # CMake 3.9+: `RPATH` settings on macOS do not affect `install_name`. +_new_cmake_policy(CMP0075) # CMake 3.12+: Include file check macros honor `CMAKE_REQUIRED_LIBRARIES` +_new_cmake_policy(CMP0077) # CMake 3.13+: option() honors normal variables. diff --git a/cmake/ProjectFuncs.cmake b/cmake/ProjectFuncs.cmake new file mode 100644 index 0000000..28158ab --- /dev/null +++ b/cmake/ProjectFuncs.cmake @@ -0,0 +1,26 @@ +function(nanosm_target target) + target_compile_definitions(${target} PRIVATE "$<$:NANOSM_DEBUG>") + target_compile_features(${target} PUBLIC cxx_std_20) + target_include_directories(${target} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) +endfunction() + +function(nanosm_set_alternate_linker) + find_program(LINKER_EXECUTABLE ld.${NANOSM_LINKER} ${NANOSM_LINKER}) + if(LINKER_EXECUTABLE) + message(STATUS "Using ${NANOSM_LINKER} as argument to -fuse-ld=") + else() + message(FATAL_ERROR "Linker ${NANOSM_LINKER} does not exist on your system. Please specify one which does or omit this option from your configure command.") + endif() +endfunction() + +# set the default linker based on compiler id, if one is not provided +# This is provided so that it can be overridden +if(NOT NANOSM_LINKER AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + set(NANOSM_LINKER "lld") +elseif(NOT NANOSM_LINKER) + set(NANOSM_LINKER "bfd") +endif() + +nanosm_set_alternate_linker() + + diff --git a/doc/nanosm.toml b/doc/nanosm.toml index a85ea12..be73f0a 100644 --- a/doc/nanosm.toml +++ b/doc/nanosm.toml @@ -2,17 +2,19 @@ [nanosm] # The window manager you want to use. This is the first application -# launched, and this will always be true +# launched window-manager="/path/to/wm/binary --any-additional-args-here" # Enable verbose debug logging. Only useful for debugging issues. verbose=false -# Restart delay in seconds. +# Restart delay in seconds. restart-delay=1 # Any applications besides your window manager you want to run at startup. -# Note that applications are executed in the order they are declared. +# Note that applications are executed in the order they are declared, but +# this will not hold true if any (or all apps) crash (they will be restarted +# effectively in a psuedorandom order). [nanosm.apps] lxpanel = { command = "lxpanel" } pcmanfm-desktop = { command = "pcmanfm --desktop" } diff --git a/src/EventLoop.cpp b/src/EventLoop.cpp new file mode 100644 index 0000000..672f87d --- /dev/null +++ b/src/EventLoop.cpp @@ -0,0 +1,121 @@ +#include "EventLoop.hpp" + +#include + +namespace nanosm { + + /// A little ergonomic wrapper over + /// std::unique_ptr, for a "kinda-vector" + /// that lives on the heap and is statically sized + template + struct UniqueArray final { + explicit UniqueArray(usize size) + : array(std::make_unique(size)), + size(size) { + } + + UniqueArray(UniqueArray&& move) { + array = std::move(move.array); + size = move.size; + + // invalidate + move.array = nullptr; + move.size = 0; + } + + T& operator[](usize index) { return Get()[index]; } + const T& operator[](usize index) const { return Get()[index]; } + + T* Get() { return array.get(); } + const T* Get() const { return array.get(); } + usize Size() const { return size; } + + private: + std::unique_ptr array {}; + usize size {}; + }; + + EventLoop::EventLoop() { + epollFd = epoll_create1(EPOLL_CLOEXEC); + + if(epollFd == -1) { + perror("You Banned From Epoll, Rules"); + } + } + + EventLoop::~EventLoop() { + close(epollFd); + } + + void EventLoop::Post(PostFn func) { + if(!func) + return; + postCallbacks.push_back(func); + } + + void EventLoop::AddObject(IoObject::Ptr obj) { + if(!obj) + return; + + if(obj->GetFD() == -1) + return; + + ioObjects.insert(obj); + + epoll_event ev {}; + + ev.events = obj->InterestedEvents(); + ev.data.fd = obj->GetFD(); + + epoll_ctl(epollFd, EPOLL_CTL_ADD, obj->GetFD(), &ev); + } + + void EventLoop::RemoveObject(IoObject::Ptr obj) { + if(!obj) + return; + + ioObjects.erase(obj); + epoll_event ev {}; + + ev.events = obj->InterestedEvents(); + ev.data.fd = obj->GetFD(); + + epoll_ctl(epollFd, EPOLL_CTL_DEL, obj->GetFD(), &ev); + } + + void EventLoop::Run() { + UniqueArray events { 16 }; + + while(!shouldStop) { + auto nevents = epoll_wait(epollFd, events.Get(), events.Size(), 10); + if(nevents == -1) { + perror("epoll_wait"); + break; + } + + // All OK, let's check for events now + + for(int i = 0; i < nevents; ++i) { + for(auto pollable : ioObjects) { + if(auto fd = pollable->GetFD(); fd != -1) { + // Signal any events that occur for this + if(events[i].data.fd == fd) + pollable->OnReady(events[i].events); + } + } + } + + // Run the topmost callback once every event loop iteration. + if(!postCallbacks.empty()) { + auto& frontCallback = postCallbacks.front(); + frontCallback(); + postCallbacks.pop_front(); + } + } + } + + void EventLoop::Stop() { + shouldStop = true; + } + +} // namespace nanosm diff --git a/src/EventLoop.hpp b/src/EventLoop.hpp index 4f2a1fd..8278104 100644 --- a/src/EventLoop.hpp +++ b/src/EventLoop.hpp @@ -1,34 +1,68 @@ -#include +#include + +#include #include +#include +#include +#include + +#include "Types.hpp" namespace nanosm { - /// The nanosm event loop. Pretty barren. + /// The nanosm event loop. Not that barren, but basically + /// just enough to have the program working. struct EventLoop { - - /// A pollable object. - struct Pollable { - virtual ~Pollable() = default; - - /// Returns the FD. May return nullopt if there is no active file descriptor - /// for this polled object (this simply means you won't get events until there is one) - virtual std::optional GetFD() const = 0; + using PostFn = std::function; - /// Called when the object is ready (do any i/o or handling here) - virtual void OnReady() = 0; + /// A IO object. + struct IoObject { + using Ptr = std::shared_ptr; + + explicit IoObject(EventLoop& assoc) + : eventLoop(assoc) { + } + + virtual ~IoObject() = default; + + /// Returns the file descriptor for this IO object. + virtual int GetFD() const = 0; + + /// Return raw event mask + virtual int InterestedEvents() const = 0; + + /// Called when the object is ready (do any i/o or handling here) + /// [eventMask] is the raw epoll event mask + virtual void OnReady(int eventMask) = 0; + + protected: + EventLoop& eventLoop; }; EventLoop(); ~EventLoop(); /// Add an object to the epoll event loop. - void AddObject(int fd, std::shared_ptr obj); + void AddObject(IoObject::Ptr obj); + + void RemoveObject(IoObject::Ptr obj); + + /// Posts a function to run after the epoll events are dispatched. + /// This queue of functions is mutually exclusive, so is also usable by + /// I/O objects to post completion notifications/callbacks. + void Post(PostFn func); /// Runs the main loop. void Run(); - private: - int epollFd{}; + void Stop(); + + private: + int epollFd {}; + bool shouldStop { false }; + + std::set> ioObjects {}; + std::deque postCallbacks {}; }; -} +} // namespace nanosm diff --git a/src/Types.hpp b/src/Types.hpp new file mode 100644 index 0000000..8ac1e2c --- /dev/null +++ b/src/Types.hpp @@ -0,0 +1,13 @@ +#pragma once +#include + +using u8 = std::uint8_t; +using i8 = std::int8_t; +using u16 = std::uint16_t; +using i16 = std::int16_t; +using u32 = std::uint32_t; +using i32 = std::int32_t; +using u64 = std::uint64_t; +using i64 = std::int64_t; +using usize = std::size_t; +using isize = std::intptr_t; diff --git a/src/WordExp.cpp b/src/WordExp.cpp new file mode 100644 index 0000000..eb50dbb --- /dev/null +++ b/src/WordExp.cpp @@ -0,0 +1,57 @@ +#include "WordExp.hpp" + +#include "Types.hpp" + +namespace nanosm { + WordExp WordExp::Expand(std::string_view string) { + WordExp res {}; + + usize startIndex {}; + usize endIndex {}; + bool inQuotes { false }; + + auto len = [&]() { return (endIndex - startIndex); }; + // auto currentWord = [&]() { return std::string(string.data() + startIndex, len()); }; + auto addWord = [&](auto nextIndex) { + // a bit of a HACK. should be fixed properly, I think + if(len() > 1) + res.words.emplace_back(string.data() + startIndex, len()); + + startIndex = nextIndex; + endIndex = startIndex; + }; + + for(usize i = 0; i < string.length(); ++i) { + switch(string[i]) { + case '"': + if(!inQuotes) { + inQuotes = true; + startIndex = i + 1; + } else { + inQuotes = false; + addWord(i + 1); + } + + endIndex++; + break; + + case ' ': + // Spaces are allowed in quoted strings + if(!inQuotes) + addWord(i + 1); + else + endIndex++; + break; + + default: endIndex++; break; + } + } + + // Add the last word + if(len() != 0) + addWord(0); + + return res; + } + +} // namespace nanosm diff --git a/src/WordExp.hpp b/src/WordExp.hpp new file mode 100644 index 0000000..a3ed884 --- /dev/null +++ b/src/WordExp.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +namespace nanosm { + + /// A kind-of reimplementation of wordexp(3) in C++, since it's rife with security issues, + /// annoying, and very poorly implemented (one implementation actually + /// ends up spawning a shell, that end up running Perl code. I'm not kidding.) + struct WordExp { + std::vector words; + + /// Expand a string (e.g "hello world \"testing 1234\"") into individual parts. + /// This function also respects quotation marks. + /// + /// This function does NOT: + /// - Expand environment strings (e.g: $PWD or etc.) + /// - Expand shell ~~injection~~ strings (eg `uname -r`) + /// + /// For the sample input, the returned object's `words` vector would look like: + /// [0]: hello + /// [1]: world + /// [2]: testing 1234 + static WordExp Expand(std::string_view string); + }; + +} // namespace nanosm diff --git a/src/main.cpp b/src/main.cpp index 970d628..3777542 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,196 @@ +#include + +#include +#include + #include "EventLoop.hpp" +#include "WordExp.hpp" + +nanosm::EventLoop ev; + +#include +#include +#include +#include +#include + +struct TestProcess : nanosm::EventLoop::IoObject, std::enable_shared_from_this { + // bring ctor in + using nanosm::EventLoop::IoObject::IoObject; + + virtual ~TestProcess() { + Kill(); + Reset(); + } + + int GetFD() const override { + return pidfdFd; + } + + int InterestedEvents() const override { + /// Pidfd only returns EPOLLIN. + return EPOLLIN; + } + + void Spawn(const std::string& commandLine) { + commLine = commandLine; + Respawn(); + } + + void Respawn() { + pid = Clone3({ + .flags = CLONE_PIDFD, + .pidfd = std::bit_cast(&pidfdFd), + .exit_signal = SIGCHLD, + }); + + if(pid < 0) + perror("Error cloning"); + + if(pid == 0) { + // Forked from the parent successfully, execute the given thing + auto exp = nanosm::WordExp::Expand(commLine); + std::vector argv(exp.words.size()); + for(usize i = 0; i < exp.words.size(); ++i) + argv[i] = exp.words[i].data(); + + execvp(exp.words[0].data(), argv.data()); + } else { + // Parent: monitor FD + eventLoop.AddObject(IoObject::Ptr(shared_from_this())); + } + } + + void OnReady(int eventMask) override { + // In our case, any readiness signaled by the pidfd means the process exited + // so this will never block (or really, wait). + waitid(P_PIDFD, pidfdFd, &siginfo, WNOHANG); + + // Post a callback to call the user's + eventLoop.Post([self = shared_from_this()]() { + // Prepare for re-attaching, or etc. + self->eventLoop.RemoveObject(self); + self->Reset(); + + if(self->onProcessExit) + self->onProcessExit(self->siginfo.si_status); + }); + } + + void Kill() { + if(pid != -1) + kill(pid, SIGTERM); + } + + void SetExitCallback(std::function f) { + onProcessExit = f; + } + + private: + static pid_t Clone3(const clone_args& args) { + return syscall(SYS_clone3, &args, sizeof(clone_args)); + } + + void Reset() { + if(pidfdFd != -1) { + close(pidfdFd); + pidfdFd = -1; + } + + pid = -1; + siginfo = {}; + } + + int pidfdFd { -1 }; + pid_t pid { -1 }; + siginfo_t siginfo {}; + std::string commLine; + + std::function onProcessExit; +}; + +#include + +struct TestTimer : nanosm::EventLoop::IoObject, std::enable_shared_from_this { + TestTimer(nanosm::EventLoop& ev) + : nanosm::EventLoop::IoObject(ev) { + timerFd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + if(timerFd < 0) + perror("timerfd_create"); + } + + virtual ~TestTimer() { + Reset(); + } + + void Reset() { + if(timerFd != -1) + close(timerFd); + } + + int GetFD() const override { + return timerFd; + } + + int InterestedEvents() const override { + /// TimerFD only returns EPOLLIN. + return EPOLLIN; + } + + void OnReady(int eventMask) override { + u64 expiryCount {}; + + // Read the amount of timer expires so it stops screaming. + read(timerFd, &expiryCount, sizeof(expiryCount)); + + // Post the expiry callback directly into the event loop + if(cb) + eventLoop.Post(cb); + } + + void SetExpiryCallback(std::function expire) { + cb = expire; + } + + void Arm(u32 durationSeconds) { + itimerspec spec {}; + spec.it_value.tv_sec = durationSeconds; + + // TODO: validate. + timerfd_settime(timerFd, 0, &spec, nullptr); + } + + private: + int timerFd { -1 }; + std::function cb; +}; + +// tests stuff :) + +auto timer = std::make_shared(ev); + +void test() { + auto process = std::make_shared(ev); + + // Do magic + process->SetExitCallback([p = process](int exitCode) { + printf("exited with %d exitcode\n", exitCode); + + timer->SetExpiryCallback([pp = p]() { + printf("5s elapsed, restarting Nowr\n"); + pp->Respawn(); + }); + + // Start the timer to wait a bit before restarting the process + timer->Arm(5); + }); + + ev.AddObject(timer); + process->Spawn("xterm"); +} int main(int argc, char** argv) { - nanosm::EventLoop ev; + ev.Post(test); ev.Run(); return 0;