From fc3a513adab7e76ded8b593579d4f5184f94f87d Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 18 Mar 2026 01:25:05 -0500 Subject: [PATCH] Multi-instance app support added. --- apps/clock/clock.c | 9 +-- dvxshell/shellApp.c | 160 +++++++++++++++++++++++++++++++++++++++++++- dvxshell/shellApp.h | 2 + 3 files changed, 166 insertions(+), 5 deletions(-) diff --git a/apps/clock/clock.c b/apps/clock/clock.c index c968a42..88d9a64 100644 --- a/apps/clock/clock.c +++ b/apps/clock/clock.c @@ -64,10 +64,11 @@ static void updateTime(void); // TS_PRIORITY_LOW because clock updates are cosmetic and should never // preempt interactive apps or the shell's event processing. AppDescriptorT appDescriptor = { - .name = "Clock", - .hasMainLoop = true, - .stackSize = 0, - .priority = TS_PRIORITY_LOW + .name = "Clock", + .hasMainLoop = true, + .multiInstance = true, + .stackSize = 0, + .priority = TS_PRIORITY_LOW }; // ============================================================ diff --git a/dvxshell/shellApp.c b/dvxshell/shellApp.c index 63bab99..e849ffd 100644 --- a/dvxshell/shellApp.c +++ b/dvxshell/shellApp.c @@ -30,6 +30,10 @@ int32_t sCurrentAppId = 0; static int32_t allocSlot(void); static void appTaskWrapper(void *arg); static const char *baseName(const char *path); +static void cleanupTempFile(ShellAppT *app); +static int32_t copyFile(const char *src, const char *dst); +static bool isPathLoaded(const char *path); +static int32_t makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize); void shellAppInit(void); void shellForceKillApp(AppContextT *ctx, ShellAppT *app); ShellAppT *shellGetApp(int32_t appId); @@ -88,6 +92,96 @@ static const char *baseName(const char *path) { return slash ? slash + 1 : path; } + +// Delete the temp file for a multi-instance app copy, if one exists. +static void cleanupTempFile(ShellAppT *app) { + if (app->tempPath[0] != '\0') { + remove(app->tempPath); + app->tempPath[0] = '\0'; + } +} + + +// Binary file copy. Returns 0 on success, -1 on failure. +static int32_t copyFile(const char *src, const char *dst) { + FILE *in = fopen(src, "rb"); + + if (!in) { + return -1; + } + + FILE *out = fopen(dst, "wb"); + + if (!out) { + fclose(in); + return -1; + } + + char buf[4096]; + size_t n; + + while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { + if (fwrite(buf, 1, n, out) != n) { + fclose(in); + fclose(out); + remove(dst); + return -1; + } + } + + fclose(in); + fclose(out); + return 0; +} + + +// Check if a DXE path is already loaded in any active slot. +static bool isPathLoaded(const char *path) { + for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + if (sApps[i].state != AppStateFreeE && strcmp(sApps[i].path, path) == 0) { + return true; + } + } + + return false; +} + + +// Build a temp path for a multi-instance copy. Uses the TEMP or TMP +// environment variable if set, otherwise falls back to the current directory. +// The slot ID is embedded in the filename to ensure uniqueness. +// Example: TEMP=C:\TEMP -> "C:\TEMP\_dvx02.app" +static int32_t makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize) { + // Find extension from original path + const char *lastSlash = strrchr(origPath, '/'); + const char *lastBack = strrchr(origPath, '\\'); + + if (lastBack > lastSlash) { + lastSlash = lastBack; + } + + const char *dot = strrchr(origPath, '.'); + + if (!dot || (lastSlash && dot < lastSlash)) { + dot = origPath + strlen(origPath); + } + + const char *tmpDir = getenv("TEMP"); + + if (!tmpDir) { + tmpDir = getenv("TMP"); + } + + if (tmpDir && tmpDir[0]) { + snprintf(out, outSize, "%s/_dvx%02ld%s", tmpDir, (long)id, dot); + } else { + snprintf(out, outSize, "_dvx%02ld%s", (long)id, dot); + } + + return 0; +} + + // ============================================================ // Public API (alphabetical) // ============================================================ @@ -129,6 +223,7 @@ void shellForceKillApp(AppContextT *ctx, ShellAppT *app) { app->dxeHandle = NULL; } + cleanupTempFile(app); app->state = AppStateFreeE; shellLog("Shell: force-killed app '%s'", app->name); } @@ -152,6 +247,14 @@ ShellAppT *shellGetApp(int32_t appId) { // Each .app file is a DXE3 shared object that exports _appDescriptor and // _appMain (and optionally _appShutdown). The leading underscore is the // COFF symbol convention; DJGPP's dlsym expects it. +// +// Multi-instance support: DXE3's dlopen returns the same handle for the +// same path (reference-counted), so two loads of the same .dxe share all +// global/static state — only one main loop runs and closing one kills both. +// If the app's descriptor sets multiInstance=true, we copy the .dxe to a +// unique temp file before dlopen, giving each instance its own code+data. +// If multiInstance=false (the default), a second load of the same path is +// blocked with an error message. int32_t shellLoadApp(AppContextT *ctx, const char *path) { // Allocate a slot int32_t id = allocSlot(); @@ -161,13 +264,56 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { return -1; } + // Check if this DXE is already loaded. If so, we need to inspect its + // descriptor to decide whether to allow a second instance. + const char *loadPath = path; + char tempPath[260] = {0}; + + if (isPathLoaded(path)) { + // Peek at the descriptor from the already-loaded module to check + // multiInstance. Since it's already loaded, dlopen returns the + // existing handle (cheap, no actual reload). + void *peek = dlopen(path, RTLD_GLOBAL); + + if (peek) { + AppDescriptorT *peekDesc = (AppDescriptorT *)dlsym(peek, "_appDescriptor"); + bool allow = peekDesc && peekDesc->multiInstance; + dlclose(peek); + + if (!allow) { + char msg[320]; + snprintf(msg, sizeof(msg), "%s is already running.", baseName(path)); + dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); + return -1; + } + } + + // Multi-instance allowed: copy to a temp file so dlopen gets + // an independent code+data image. + makeTempPath(path, id, tempPath, sizeof(tempPath)); + + if (copyFile(path, tempPath) != 0) { + char msg[320]; + snprintf(msg, sizeof(msg), "Failed to create instance copy of %s.", baseName(path)); + dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); + return -1; + } + + loadPath = tempPath; + } + // Load the DXE - void *handle = dlopen(path, RTLD_GLOBAL); + void *handle = dlopen(loadPath, RTLD_GLOBAL); if (!handle) { char msg[512]; snprintf(msg, sizeof(msg), "Failed to load %s:\n%s", baseName(path), dlerror()); dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); + + if (tempPath[0]) { + remove(tempPath); + } + return -1; } @@ -179,6 +325,11 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { snprintf(msg, sizeof(msg), "%s: missing appDescriptor", baseName(path)); dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); dlclose(handle); + + if (tempPath[0]) { + remove(tempPath); + } + return -1; } @@ -189,6 +340,11 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { snprintf(msg, sizeof(msg), "%s: missing appMain", baseName(path)); dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); dlclose(handle); + + if (tempPath[0]) { + remove(tempPath); + } + return -1; } @@ -201,6 +357,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { app->appId = id; snprintf(app->name, SHELL_APP_NAME_MAX, "%s", desc->name); snprintf(app->path, sizeof(app->path), "%s", path); + snprintf(app->tempPath, sizeof(app->tempPath), "%s", tempPath); app->dxeHandle = handle; app->hasMainLoop = desc->hasMainLoop; app->entryFn = entry; @@ -304,6 +461,7 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) { app->dxeHandle = NULL; } + cleanupTempFile(app); shellLog("Shell: reaped app '%s'", app->name); app->state = AppStateFreeE; } diff --git a/dvxshell/shellApp.h b/dvxshell/shellApp.h index 810cc82..3afdeb7 100644 --- a/dvxshell/shellApp.h +++ b/dvxshell/shellApp.h @@ -44,6 +44,7 @@ typedef struct { char name[SHELL_APP_NAME_MAX]; bool hasMainLoop; + bool multiInstance; // true = allow multiple instances via temp copy int32_t stackSize; // 0 = TS_DEFAULT_STACK_SIZE int32_t priority; // TS_PRIORITY_* or custom } AppDescriptorT; @@ -84,6 +85,7 @@ typedef struct { int32_t appId; // unique ID = slot index (1-based; 0 = shell) char name[SHELL_APP_NAME_MAX]; char path[260]; + char tempPath[260]; // temp copy path for multi-instance (empty = not a copy) void *dxeHandle; // dlopen() handle AppStateE state; bool hasMainLoop;