Fix .bat build script. Cleanup .dll.hotreload.live.N binaries on startup.
This commit is contained in:
1 parent
b4e77ed401
commit
9da91ed940
6 files changed
+174
-12
No files matched your search
+21
-5
@@ -7,8 +7,7 @@ REM
|
|||||||
REM BUILD_TYPE Debug^|Release^|RelWithDebInfo^|MinSizeRel (default: Release)
|
REM BUILD_TYPE Debug^|Release^|RelWithDebInfo^|MinSizeRel (default: Release)
|
||||||
REM
|
REM
|
||||||
REM Anything after BUILD_TYPE is passed straight through to cmake's
|
REM Anything after BUILD_TYPE is passed straight through to cmake's
|
||||||
REM configure step (up to 8 extra arguments -- %2 through %9, batch's own
|
REM configure step, e.g.:
|
||||||
REM positional-parameter limit), e.g.:
|
|
||||||
REM scripts\build.bat Debug -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON
|
REM scripts\build.bat Debug -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON
|
||||||
REM CMake caches -D options across reconfigures, so passing them once and
|
REM CMake caches -D options across reconfigures, so passing them once and
|
||||||
REM then calling scripts\build.bat plain afterward for incremental builds
|
REM then calling scripts\build.bat plain afterward for incremental builds
|
||||||
@@ -20,10 +19,27 @@ setlocal enabledelayedexpansion
|
|||||||
cd /d "%~dp0.."
|
cd /d "%~dp0.."
|
||||||
|
|
||||||
if "%BUILD_DIR%"=="" set "BUILD_DIR=out\build"
|
if "%BUILD_DIR%"=="" set "BUILD_DIR=out\build"
|
||||||
set "BUILD_TYPE=%~1"
|
|
||||||
if "%BUILD_TYPE%"=="" set "BUILD_TYPE=Release"
|
|
||||||
if "%TOOLCACHE_DIR%"=="" set "TOOLCACHE_DIR=.cache\tools"
|
if "%TOOLCACHE_DIR%"=="" set "TOOLCACHE_DIR=.cache\tools"
|
||||||
set "EXTRA_CMAKE_ARGS=%2 %3 %4 %5 %6 %7 %8 %9"
|
|
||||||
|
REM Deliberately NOT %1/%2/... for this: cmd.exe's own parameter
|
||||||
|
REM parser treats = (along with , and ;) as an argument separator, so
|
||||||
|
REM "-DBUILD_EXAMPLES=ON" typed as one argument arrives as %2=
|
||||||
|
REM "-DBUILD_EXAMPLES" and %3="ON" -- two arguments, with the = gone --
|
||||||
|
REM before this script ever sees it, corrupting any -D value passed
|
||||||
|
REM this way and silently breaking the configure step below. %* does
|
||||||
|
REM not go through that same parser and reflects the command line
|
||||||
|
REM as typed, and FOR /F's own tokenizer only splits on space/tab, so
|
||||||
|
REM routing %* through "tokens=1,*" correctly separates just the
|
||||||
|
REM first word from everything after it, = signs and all -- verified
|
||||||
|
REM directly (via Wine's cmd.exe) against exactly the failure the
|
||||||
|
REM old %2 %3 %4... version had, not just reasoned through.
|
||||||
|
set "BUILD_TYPE=Release"
|
||||||
|
set "EXTRA_CMAKE_ARGS="
|
||||||
|
for /f "tokens=1,*" %%A in ("%*") do (
|
||||||
|
set "BUILD_TYPE=%%A"
|
||||||
|
set "EXTRA_CMAKE_ARGS=%%B"
|
||||||
|
)
|
||||||
|
if "%BUILD_TYPE%"=="" set "BUILD_TYPE=Release"
|
||||||
|
|
||||||
where cmake >nul 2>nul
|
where cmake >nul 2>nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
|
|||||||
@@ -475,4 +475,16 @@ fix this in any shared library, hot-reloaded or not.
|
|||||||
copy internally specifically so your build system's exact
|
copy internally specifically so your build system's exact
|
||||||
write pattern can't cause a stale reload (a known gotcha for this
|
write pattern can't cause a stale reload (a known gotcha for this
|
||||||
style of tool; see `HotReloadContext.cpp`'s
|
style of tool; see `HotReloadContext.cpp`'s
|
||||||
`stagePathForLoad()`).
|
`stagePathForLoad()`). On Windows, each staged copy backing a
|
||||||
|
still-loaded module can't be deleted for as long as that module
|
||||||
|
stays mapped -- the rest of the process's lifetime, by design --
|
||||||
|
so a long-running session accumulates one such file per reload;
|
||||||
|
`hot_reload_load()` sweeps up whatever a *previous* run left
|
||||||
|
behind, so this doesn't grow without bound across sessions, only
|
||||||
|
(unavoidably) within one very long one. See
|
||||||
|
`source/examples/HotReloadLiveDemo/README.md`'s own section on
|
||||||
|
this for the full story, including why "Rebuild Solution" (or any
|
||||||
|
other full clean) separately fails outright while a host built
|
||||||
|
from this library is running, for an unrelated reason -- it also
|
||||||
|
tries to delete the *host's own* executable, which Windows refuses
|
||||||
|
while it's running, and which has no staging workaround at all.
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "MemoryProtection.hpp"
|
#include "MemoryProtection.hpp"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
@@ -26,6 +27,56 @@ namespace hot_reload
|
|||||||
out << in.rdbuf();
|
out << in.rdbuf();
|
||||||
return static_cast<bool>(out);
|
return static_cast<bool>(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Best-effort: deletes any staging copy left over from a
|
||||||
|
// PREVIOUS run of this same host/module -- see
|
||||||
|
// stagePathForLoad()'s own comment for why those copies exist
|
||||||
|
// at all. On POSIX this normally finds nothing (an active
|
||||||
|
// run's own copies are already gone by the time it makes its
|
||||||
|
// next one -- see the std::remove() calls after each
|
||||||
|
// ModuleHandle::load() below). On Windows, a copy backing a
|
||||||
|
// still-loaded module cannot be deleted for as long as that
|
||||||
|
// module stays mapped -- which, by this library's own design,
|
||||||
|
// is for the rest of the CURRENT process's lifetime (see
|
||||||
|
// HotReloadContext.hpp's LoadedModule comment) -- so within a
|
||||||
|
// single long-running session some accumulation is expected
|
||||||
|
// and unavoidable; this specifically cleans up the PREVIOUS
|
||||||
|
// session's leftovers once that process has exited and
|
||||||
|
// released whatever lock Windows held. Called once, here, at
|
||||||
|
// the first hot_reload_load() for a given name -- not on every
|
||||||
|
// reload() -- since a copy from earlier in the SAME session
|
||||||
|
// would still be locked for the same reason and retrying
|
||||||
|
// wouldn't change that.
|
||||||
|
void cleanupOrphanedStagingFiles(const std::string& sourcePath, const std::string& moduleName)
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
fs::path source(sourcePath);
|
||||||
|
fs::path dir = source.has_parent_path() ? source.parent_path() : fs::path(".");
|
||||||
|
std::string prefix = source.filename().string() + ".hotreload." + moduleName + ".";
|
||||||
|
|
||||||
|
std::error_code dirError;
|
||||||
|
fs::directory_iterator entries(dir, dirError);
|
||||||
|
if (dirError)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fs::directory_entry& entry : entries)
|
||||||
|
{
|
||||||
|
std::error_code fileTypeError;
|
||||||
|
if (!entry.is_regular_file(fileTypeError) || fileTypeError)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string filename = entry.path().filename().string();
|
||||||
|
if (filename.compare(0, prefix.size(), prefix) == 0)
|
||||||
|
{
|
||||||
|
std::error_code removeError;
|
||||||
|
fs::remove(entry.path(), removeError); // best-effort; a failure here just means try again next launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HotReloadContext::HotReloadContext(const HotReloadOptions* options)
|
HotReloadContext::HotReloadContext(const HotReloadOptions* options)
|
||||||
@@ -89,6 +140,8 @@ namespace hot_reload
|
|||||||
return HOT_RELOAD_ERROR_INVALID_ARGUMENT;
|
return HOT_RELOAD_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupOrphanedStagingFiles(path, moduleName);
|
||||||
|
|
||||||
LoadedModule module;
|
LoadedModule module;
|
||||||
std::string staged = stagePathForLoad(module, moduleName, path);
|
std::string staged = stagePathForLoad(module, moduleName, path);
|
||||||
if (staged.empty())
|
if (staged.empty())
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ int main(int argc, char** argv)
|
|||||||
std::printf(" (or run watch.sh, in this example's own directory, in another\n");
|
std::printf(" (or run watch.sh, in this example's own directory, in another\n");
|
||||||
std::printf(" terminal to rebuild automatically every time you save)\n");
|
std::printf(" terminal to rebuild automatically every time you save)\n");
|
||||||
std::printf(" Stop: Ctrl+C\n");
|
std::printf(" Stop: Ctrl+C\n");
|
||||||
|
std::printf(" Note: rebuild the PLUGIN target above, not \"Rebuild Solution\"/a full\n");
|
||||||
|
std::printf(" clean -- that also tries to delete this Host's own .exe, which\n");
|
||||||
|
std::printf(" Windows refuses to do while it's running (see this example's\n");
|
||||||
|
std::printf(" own README for why that's expected, not a bug)\n");
|
||||||
std::printf("================================================================\n\n");
|
std::printf("================================================================\n\n");
|
||||||
|
|
||||||
std::signal(SIGINT, &handleSigint);
|
std::signal(SIGINT, &handleSigint);
|
||||||
|
|||||||
@@ -73,9 +73,51 @@ HotReloadContext.cpp`) copies a plugin to a fresh, uniquely-named file
|
|||||||
before ever calling `LoadLibrary`/`dlopen` on it -- built originally to
|
before ever calling `LoadLibrary`/`dlopen` on it -- built originally to
|
||||||
sidestep a dlopen caching gotcha on POSIX (see that function's
|
sidestep a dlopen caching gotcha on POSIX (see that function's
|
||||||
comment) -- so the path this example's build actually writes to is
|
comment) -- so the path this example's build actually writes to is
|
||||||
never the one Windows locks. This reasoning was not verified against
|
never the one Windows locks. Confirmed directly on real Windows/MSVC:
|
||||||
a real Windows rebuild-while-running cycle specifically (unlike the
|
rebuilding `hotreload_live_plugin` repeatedly while the Host stayed
|
||||||
rest of this library's Windows behavior, which was exercised under
|
running worked correctly, hot-reloading each time.
|
||||||
Wine -- see `source/HotReload/README.md`'s verification notes); if a
|
|
||||||
rebuild on Windows still fails while the Host is running, that
|
## "Rebuild Solution" (or any other full clean) fails while the Host is running -- this is expected
|
||||||
mismatch between reasoning and reality is worth reporting.
|
|
||||||
|
Different from the DLL case just above, and this one genuinely can't
|
||||||
|
be worked around the same way. Visual Studio's "Rebuild" (and the
|
||||||
|
equivalent `cmake --build --clean-first`, or deleting the build
|
||||||
|
directory yourself) deletes *every* build output first, including
|
||||||
|
`hotreload_live_demo_host.exe` itself -- and Windows will not let you
|
||||||
|
delete a running process's own executable image, full stop, for as
|
||||||
|
long as it's running. There's no staging trick that helps here the way
|
||||||
|
there is for the plugin DLL: the thing being deleted is quite literally
|
||||||
|
the file currently executing.
|
||||||
|
|
||||||
|
Confirmed directly: this fails with `ninja: error: remove(bin/
|
||||||
|
examples/hotreload_live_demo_host.exe): Access is denied.`, and the
|
||||||
|
"Rebuild failed" it's reported as makes it look worse than it is --
|
||||||
|
depending on your build tool, the *subsequent* build step can still
|
||||||
|
run and still correctly rebuild+reload the plugin regardless (also
|
||||||
|
confirmed directly), since only the clean step, not the whole
|
||||||
|
operation, actually failed.
|
||||||
|
|
||||||
|
**The fix is simply not to use "Rebuild"/a full clean while the Host
|
||||||
|
is running** -- an ordinary incremental "Build", or building just
|
||||||
|
`hotreload_live_plugin` specifically (exactly what `watch.sh` and the
|
||||||
|
manual `cmake --build ... --target hotreload_live_plugin` command
|
||||||
|
above already do), never touches the Host's own executable at all, so
|
||||||
|
neither is affected by this. Stop the Host first if you ever do want a
|
||||||
|
genuine full clean.
|
||||||
|
|
||||||
|
## Leftover `hotreload_live_plugin.dll.hotreload.live.N` files
|
||||||
|
|
||||||
|
Each reload stages the plugin through a fresh, uniquely-named copy
|
||||||
|
before loading it (`HotReloadContext::stagePathForLoad()`, needed for
|
||||||
|
reasons unrelated to this -- see that function's comment). On Linux/
|
||||||
|
macOS, the copy is deleted right after loading and nothing
|
||||||
|
accumulates. On Windows, that deletion fails for as long as the module
|
||||||
|
it backs stays loaded -- which, by this library's design, is for the
|
||||||
|
rest of the Host process's lifetime, so **one leftover file per reload
|
||||||
|
during a long editing session is expected**, not a leak to chase down.
|
||||||
|
`hot_reload_load()` sweeps up whatever a *previous* run of the same
|
||||||
|
Host left behind the next time it starts, so restarting the Host is
|
||||||
|
enough to clean up between sessions; within a single very long
|
||||||
|
session, some accumulation is simply the cost of every old version
|
||||||
|
staying safely addressable. Safe to delete by hand at any point the
|
||||||
|
Host isn't running, if it bothers you before then.
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -182,6 +184,39 @@ int main()
|
|||||||
CHECK(fixtureError != nullptr && std::strlen(fixtureError) > 0);
|
CHECK(fixtureError != nullptr && std::strlen(fixtureError) > 0);
|
||||||
hot_reload_context_destroy(fixtureCtx);
|
hot_reload_context_destroy(fixtureCtx);
|
||||||
|
|
||||||
|
// --- orphaned staging-file cleanup: a real, reported case. On
|
||||||
|
// Windows, a staged copy backing a still-loaded module cannot be
|
||||||
|
// deleted for as long as that module stays mapped (the rest of
|
||||||
|
// the process's lifetime, by this library's own design), so a
|
||||||
|
// long-running session accumulates one such file per reload --
|
||||||
|
// expected and unavoidable, but hot_reload_load() should still
|
||||||
|
// sweep up whatever a PREVIOUS run of the same host left behind.
|
||||||
|
// Simulated here (rather than actually reproducing the Windows
|
||||||
|
// lock) by dropping a few files matching the staging pattern next
|
||||||
|
// to a real fixture before loading it, then checking they're gone
|
||||||
|
// afterward -- this exercises the real cleanup code path
|
||||||
|
// regardless of platform, even though the scenario it protects
|
||||||
|
// against is Windows-specific.
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
std::string orphanedBase = std::string(PLUGIN_V1_PATH) + ".hotreload.cleanup-test.";
|
||||||
|
for (int i = 1; i <= 3; ++i)
|
||||||
|
{
|
||||||
|
std::ofstream orphan(orphanedBase + std::to_string(i));
|
||||||
|
orphan << "leftover from a previous run";
|
||||||
|
}
|
||||||
|
|
||||||
|
HotReloadContext* cleanupCtx = hot_reload_context_create(nullptr);
|
||||||
|
CHECK(cleanupCtx != nullptr);
|
||||||
|
CHECK(hot_reload_load(cleanupCtx, "cleanup-test", PLUGIN_V1_PATH) == HOT_RELOAD_OK);
|
||||||
|
|
||||||
|
for (int i = 1; i <= 3; ++i)
|
||||||
|
{
|
||||||
|
CHECK(!fs::exists(orphanedBase + std::to_string(i)));
|
||||||
|
}
|
||||||
|
hot_reload_context_destroy(cleanupCtx);
|
||||||
|
}
|
||||||
|
|
||||||
if (g_failures == 0)
|
if (g_failures == 0)
|
||||||
{
|
{
|
||||||
std::printf("hotreload_tests: all checks passed\n");
|
std::printf("hotreload_tests: all checks passed\n");
|
||||||
|
|||||||
Reference in new issue
Block a user