Actually implement event loop & process stuff

Pretty easy all things considered.
This commit is contained in:
Lily Tsuru 2024-02-01 22:54:06 -05:00
parent dd0280a1e5
commit 833f2054a4
13 changed files with 614 additions and 20 deletions

View File

@ -21,6 +21,7 @@ BinPackParameters: true
BreakConstructorInitializers: BeforeColon
BreakStringLiterals: false
ColumnLimit: 0
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true

24
CMakeLists.txt Normal file
View File

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

View File

@ -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")

View File

@ -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()

22
cmake/Policies.cmake Normal file
View File

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

26
cmake/ProjectFuncs.cmake Normal file
View File

@ -0,0 +1,26 @@
function(nanosm_target target)
target_compile_definitions(${target} PRIVATE "$<$<CONFIG:DEBUG>: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()

View File

@ -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" }

121
src/EventLoop.cpp Normal file
View File

@ -0,0 +1,121 @@
#include "EventLoop.hpp"
#include <sys/epoll.h>
namespace nanosm {
/// A little ergonomic wrapper over
/// std::unique_ptr<T[]>, for a "kinda-vector"
/// that lives on the heap and is statically sized
template <class T>
struct UniqueArray final {
explicit UniqueArray(usize size)
: array(std::make_unique<T[]>(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<T[]> 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<epoll_event> 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

View File

@ -1,34 +1,68 @@
#include <memory>
#include <sys/epoll.h>
#include <deque>
#include <functional>
#include <memory>
#include <optional>
#include <set>
#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<int> GetFD() const = 0;
using PostFn = std::function<void()>;
/// 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<IoObject>;
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<Pollable> 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<std::shared_ptr<IoObject>> ioObjects {};
std::deque<PostFn> postCallbacks {};
};
}
} // namespace nanosm

13
src/Types.hpp Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
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;

57
src/WordExp.cpp Normal file
View File

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

29
src/WordExp.hpp Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include <string>
#include <string_view>
#include <vector>
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<std::string> 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

View File

@ -1,7 +1,196 @@
#include <bits/types/struct_itimerspec.h>
#include <ctime>
#include <memory>
#include "EventLoop.hpp"
#include "WordExp.hpp"
nanosm::EventLoop ev;
#include <linux/sched.h>
#include <sched.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
struct TestProcess : nanosm::EventLoop::IoObject, std::enable_shared_from_this<TestProcess> {
// 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<u64>(&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<char*> 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<void(int)> 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<void(int)> onProcessExit;
};
#include <sys/timerfd.h>
struct TestTimer : nanosm::EventLoop::IoObject, std::enable_shared_from_this<TestTimer> {
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<void()> 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<void()> cb;
};
// tests stuff :)
auto timer = std::make_shared<TestTimer>(ev);
void test() {
auto process = std::make_shared<TestProcess>(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;