mirror of
https://github.com/logos-co/logos-module-loader-qt.git
synced 2026-08-31 06:11:18 +00:00
* feat(windows): port the module host's POSIX-bound pieces Three changes, in the order they block a Windows module load. 1. qt_plugin_format_loader.cpp -- findInDir probed "logos_host_qt" and "logos_host" with no extension. On Windows the host is logos_host_qt.exe, so the probe finds nothing and EVERY module load fails before it starts: the host binary is resolved first. logos-view-module-runtime already appends ".exe" for its ui-host; this does the same. Behaviour on POSIX is byte-identical (the suffix is ""). 2. token_source.cpp -- mingw-w64 ships <unistd.h> (read/close are fine) but not <poll.h>, and the poll() here exists purely to BOUND the wait so a never-delivered token cannot hang the child. Win32 has no single readiness wait covering all three things stdin can be here -- an anonymous pipe from the subprocess container, a redirected file, or a console -- so the Windows branch does the blocking read on a thread and bounds the wait instead. The shared state is a shared_ptr so a thread that wakes after we time out writes into live memory; detaching is safe precisely because a missing token is fatal at startup and the host exits immediately after. 3. logos_host.cpp -- the crash handler is POSIX signals + sigaltstack + backtrace(3), and mingw has no <execinfo.h> (nor SIGBUS). Guarded out rather than ported: the obvious Win32 translation, SetUnhandledExceptionFilter + DbgHelp SymFromAddr, would reintroduce exactly the hazard safeWrite's comment exists to avoid -- symbolisation takes the loader lock, and a crash handler that takes the loader lock deadlocks precisely when it is needed (a fault during a module load). If Windows backtraces are wanted later, the safe shape is CaptureStackBackTrace with raw hex addresses symbolised offline, mirroring safeWriteHex. 4. qt/qt_app.cpp -- the self-pipe SIGTERM/SIGINT handling is guarded out, and this is NOT a gap: on Windows the parent asks for shutdown by posting WM_QUIT to the child's main thread, and Qt's Win32 event dispatcher already turns WM_QUIT into QCoreApplication::quit() -- the exact effect this hand-rolls on POSIX. The comment warns against the tempting ::pipe -> _pipe swap: it compiles, but QEventDispatcherWin32::registerSocketNotifier routes to WSAAsyncSelect and DISCARDS its return value, so a CRT pipe fd fails WSAENOTSOCK silently and the notifier simply never fires. Verified: preprocessor directives balance in all three files, and the POSIX branches still compile unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(windows): add an x86_64-windows cross target Every dependency here is a target-side library compiled into this one, so they all follow ${system}; there is no build-time code generator that must stay native. meta.platforms widened, Qt wrapper hook gated behind !isWindows with dontWrapQtApps set, and cmakeFlags pick up pkgs.logosQtCrossCmakeFlags. Pairs with the source port already on this branch (.exe suffix, token_source, crash handler, qt_app self-pipe). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(windows): port test_token_source to the mingw CRT mingw-w64 ships <unistd.h> but not the POSIX pipe/mkstemp APIs, so the test would not compile for the Windows target even though the production sources did: tests/test_token_source.cpp:28: '::pipe' has not been declared; did you mean '_pipe'? Three shims, all in an anonymous namespace so the POSIX path is textually unchanged: - makePipe: _pipe(fds, 4096, _O_BINARY). The mode argument is not incidental -- the token is compared byte for byte, and text mode would translate the "\r\n" the CRLF test writes on purpose, silently defeating what that test is checking. - writeFd: _write, which takes unsigned int rather than size_t. - makeTempFile: GetTempPathA + GetTempFileNameA. There is no mkstemp, and no /tmp for the hardcoded template to live in. Cleanup on that temp file becomes std::remove instead of ::unlink -- portable, and already reachable via <cstdio>. Verified: native aarch64-darwin build of logos-module-loader-qt still succeeds with the tests compiled and run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(windows): defer gtest discovery, and suffix the compat symlink Two Windows-only build failures in the same install/test tail. 1. gtest_discover_tests defaults to POST_BUILD, which RUNS the freshly linked test binary on the BUILD machine to enumerate cases. Under cross that binary is a PE, so the build host tries to execute it as a shell script: logos_module_loader_qt_tests.exe: line 3: syntax error DISCOVERY_MODE PRE_TEST moves enumeration to ctest time, where the binary is never run on the wrong platform. Natively this is a no-op beyond when discovery happens. 2. The logos_host -> logos_host_qt compatibility symlink was created without an executable suffix. On Windows the installed file is logos_host_qt.exe, so the link dangled and nixpkgs' noBrokenSymlinks check failed the derivation outright. Both the link and its target now carry CMAKE_EXECUTABLE_SUFFIX; nix/bin.nix recreates it the same way. logos_host_qt.exe now builds as a PE32+ console executable for x86_64-w64-mingw32. * chore(deps): re-pin the L1-L4 inputs to their merged revs logos-nix (L1), logos-protocol, logos-module, logos-container (L2), logos-cpp-sdk, logos-module-loader (L3) and logos-qt-sdk (L4) are all on their default branches now, so the lock can name the merged revs instead of the pre-merge branch tips it was resolving against while those PRs were open. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
5.4 KiB
C++
157 lines
5.4 KiB
C++
// =============================================================================
|
|
// Tests for the host-side TokenSource reader.
|
|
//
|
|
// The module host reads its auth token from a channel its container designates
|
|
// via --token-source (stdin | fd:<n> | file:<path>), with no dependency on any
|
|
// container implementation. These tests exercise each source over real pipes
|
|
// and a real temp file, plus the timeout and error paths.
|
|
// =============================================================================
|
|
#include <gtest/gtest.h>
|
|
#include "token_source.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#ifdef _WIN32
|
|
// Deliberately NOT <windows.h>: winnt.h declares a TOKEN_INFORMATION_CLASS
|
|
// enumerator literally named TokenSource, which collides head-on with this
|
|
// project's `namespace TokenSource` --
|
|
// winnt.h:4131: error: 'TokenSource' redeclared as different kind of entity
|
|
// Everything needed here lives in the CRT headers instead.
|
|
#include <io.h> // _pipe / _write / _mktemp_s
|
|
#include <share.h> // _SH_DENYNO
|
|
#include <sys/stat.h> // _S_IREAD / _S_IWRITE
|
|
#endif
|
|
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
namespace {
|
|
|
|
// mingw has no POSIX pipe(2): the CRT spells it _pipe and additionally wants a
|
|
// buffer size and a text/binary mode. _O_BINARY matters -- the token is
|
|
// compared byte for byte, and text mode would translate the "\r\n" the CRLF
|
|
// test deliberately writes.
|
|
#ifdef _WIN32
|
|
inline int makePipe(int fds[2]) { return ::_pipe(fds, 4096, _O_BINARY); }
|
|
inline int writeFd(int fd, const void* buf, std::size_t n) {
|
|
return ::_write(fd, buf, static_cast<unsigned int>(n));
|
|
}
|
|
#else
|
|
inline int makePipe(int fds[2]) { return ::pipe(fds); }
|
|
inline ssize_t writeFd(int fd, const void* buf, std::size_t n) {
|
|
return ::write(fd, buf, n);
|
|
}
|
|
#endif
|
|
|
|
// A temp file open for writing, plus its path. mingw has neither mkstemp nor
|
|
// a /tmp, so the Windows branch asks the OS for both. Returns -1 on failure.
|
|
#ifdef _WIN32
|
|
inline int makeTempFile(std::string& pathOut) {
|
|
// _mktemp_s rewrites the trailing XXXXXX in place. Relative to the cwd,
|
|
// which is writable while the tests run -- and unlike the POSIX branch
|
|
// there is no /tmp to reach for.
|
|
char tmpl[] = "logos_token_src_XXXXXX";
|
|
if (::_mktemp_s(tmpl, sizeof tmpl) != 0) return -1;
|
|
int fd = -1;
|
|
if (::_sopen_s(&fd, tmpl, _O_RDWR | _O_CREAT | _O_EXCL | _O_BINARY,
|
|
_SH_DENYNO, _S_IREAD | _S_IWRITE) != 0)
|
|
return -1;
|
|
pathOut = tmpl;
|
|
return fd;
|
|
}
|
|
#else
|
|
inline int makeTempFile(std::string& pathOut) {
|
|
char path[] = "/tmp/logos_token_src_XXXXXX";
|
|
const int fd = ::mkstemp(path);
|
|
if (fd >= 0) pathOut = path;
|
|
return fd;
|
|
}
|
|
#endif
|
|
|
|
// Write `data` to a fresh pipe and return the read fd (caller closes it). The
|
|
// write end is closed after writing so the reader sees EOF.
|
|
int pipeWith(const std::string& data) {
|
|
int fds[2];
|
|
if (makePipe(fds) != 0) return -1;
|
|
ssize_t off = 0;
|
|
while (off < static_cast<ssize_t>(data.size())) {
|
|
auto n = writeFd(fds[1], data.data() + off, data.size() - off);
|
|
if (n <= 0) break;
|
|
off += n;
|
|
}
|
|
::close(fds[1]); // EOF for the reader
|
|
return fds[0];
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(TokenSource, ReadsFromFdStripsNewline) {
|
|
const int rfd = pipeWith("my-secret-token\n");
|
|
ASSERT_GE(rfd, 0);
|
|
const std::string tok = TokenSource::read("fd:" + std::to_string(rfd));
|
|
::close(rfd);
|
|
EXPECT_EQ(tok, "my-secret-token");
|
|
}
|
|
|
|
TEST(TokenSource, ReadsFromFdWithoutNewlineAtEof) {
|
|
const int rfd = pipeWith("no-newline-token");
|
|
ASSERT_GE(rfd, 0);
|
|
const std::string tok = TokenSource::read("fd:" + std::to_string(rfd));
|
|
::close(rfd);
|
|
EXPECT_EQ(tok, "no-newline-token");
|
|
}
|
|
|
|
TEST(TokenSource, ReadsOnlyFirstLine) {
|
|
const int rfd = pipeWith("first-line\nsecond-line\n");
|
|
ASSERT_GE(rfd, 0);
|
|
const std::string tok = TokenSource::read("fd:" + std::to_string(rfd));
|
|
::close(rfd);
|
|
EXPECT_EQ(tok, "first-line");
|
|
}
|
|
|
|
TEST(TokenSource, ReadsFromFile) {
|
|
std::string path;
|
|
const int fd = makeTempFile(path);
|
|
ASSERT_GE(fd, 0);
|
|
const char* contents = "file-token\n";
|
|
ASSERT_GT(writeFd(fd, contents, std::strlen(contents)), 0);
|
|
::close(fd);
|
|
|
|
const std::string tok = TokenSource::read(std::string("file:") + path);
|
|
std::remove(path.c_str());
|
|
EXPECT_EQ(tok, "file-token");
|
|
}
|
|
|
|
TEST(TokenSource, UnknownSourceReturnsEmpty) {
|
|
EXPECT_TRUE(TokenSource::read("carrier-pigeon").empty());
|
|
}
|
|
|
|
TEST(TokenSource, InvalidFdSpecReturnsEmpty) {
|
|
EXPECT_TRUE(TokenSource::read("fd:notanumber").empty());
|
|
}
|
|
|
|
TEST(TokenSource, MissingFileReturnsEmpty) {
|
|
EXPECT_TRUE(TokenSource::read("file:/no/such/path/logos_token").empty());
|
|
}
|
|
|
|
TEST(TokenSource, TimesOutWhenNoDataArrives) {
|
|
// A pipe whose write end stays open and silent: the read must hit the
|
|
// timeout and return empty rather than block forever.
|
|
int fds[2];
|
|
ASSERT_EQ(makePipe(fds), 0);
|
|
const auto t0 = std::chrono::steady_clock::now();
|
|
const std::string tok = TokenSource::read("fd:" + std::to_string(fds[0]),
|
|
/*timeout_ms=*/200);
|
|
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - t0).count();
|
|
::close(fds[0]);
|
|
::close(fds[1]);
|
|
EXPECT_TRUE(tok.empty());
|
|
EXPECT_GE(elapsed, 150) << "should wait out the timeout, not return early";
|
|
EXPECT_LT(elapsed, 1500) << "should not block well past the timeout";
|
|
}
|