// shellApp.c — DVX Shell application loading, lifecycle, and reaping // // Manages DXE app loading via dlopen/dlsym, resource tracking through // sCurrentAppId, and clean teardown of both callback-only and main-loop apps. #include "shellApp.h" #include "dvxDialog.h" #include "platform/dvxPlatform.h" #include #include #include #include // ============================================================ // Module state // ============================================================ // Fixed-size app slot table. Using a static array (not dynamic) because: // 1. The number of concurrent apps is bounded by available DOS memory // 2. App IDs are slot indices, so a fixed array gives O(1) lookup // 3. No fragmentation concerns from repeated malloc/free cycles // Slot 0 is reserved (represents the shell itself); apps use slots 1..31. static ShellAppT sApps[SHELL_MAX_APPS]; int32_t sCurrentAppId = 0; // ============================================================ // Prototypes // ============================================================ 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 ShellAppT *findLoadedPath(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); int32_t shellLoadApp(AppContextT *ctx, const char *path); void shellReapApp(AppContextT *ctx, ShellAppT *app); bool shellReapApps(AppContextT *ctx); int32_t shellRunningAppCount(void); void shellTerminateAllApps(AppContextT *ctx); // ============================================================ // Static helpers // ============================================================ // Find the first free slot, starting at 1 (slot 0 is the shell). // Returns the slot index which also serves as the app's unique ID. // This linear scan is fine for 32 slots. static int32_t allocSlot(void) { for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { if (sApps[i].state == AppStateFreeE) { return i; } } return -1; } // Task entry point for main-loop apps. Runs in its own cooperative task. // Sets sCurrentAppId before calling the app's entry point so that any // GUI resources created during the app's lifetime are tagged with the // correct owner. When the app's main loop returns (normal exit), the // state is set to Terminating so the shell's main loop will reap it. // // If the app crashes (signal), the signal handler longjmps to main and // this function never completes — the task is killed externally via // shellForceKillApp + tsKill. static void appTaskWrapper(void *arg) { ShellAppT *app = (ShellAppT *)arg; sCurrentAppId = app->appId; app->entryFn(&app->dxeCtx); sCurrentAppId = 0; // App returned from its main loop — mark for reaping app->state = AppStateTerminatingE; } static const char *baseName(const char *path) { const char *slash = strrchr(path, '/'); if (!slash) { slash = strrchr(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; } // Find an active slot with the given DXE path. Returns the slot pointer, // or NULL if no running app was loaded from this path. static ShellAppT *findLoadedPath(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 &sApps[i]; } } return NULL; } // 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) // ============================================================ void shellAppInit(void) { memset(sApps, 0, sizeof(sApps)); } // Forcible kill — no shutdown hook is called. Used for crashed apps // (where running more app code would be unsafe) and for "End Task". // Cleanup order matters: windows first (removes them from the compositor), // then the task (frees the stack), then the DXE handle (unmaps the code). // If we closed the DXE first, destroying windows could call into unmapped // callback code and crash the shell. void shellForceKillApp(AppContextT *ctx, ShellAppT *app) { if (!app || app->state == AppStateFreeE) { return; } // Destroy all windows belonging to this app. Walk backwards because // dvxDestroyWindow removes the window from the stack, shifting indices. for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { if (ctx->stack.windows[i]->appId == app->appId) { dvxDestroyWindow(ctx, ctx->stack.windows[i]); } } // Kill the task if it has one if (app->hasMainLoop && app->mainTaskId > 0) { if (tsGetState(app->mainTaskId) != TaskStateTerminated) { tsKill(app->mainTaskId); } } // Close the DXE if (app->dxeHandle) { dlclose(app->dxeHandle); app->dxeHandle = NULL; } cleanupTempFile(app); app->state = AppStateFreeE; shellLog("Shell: force-killed app '%s'", app->name); } ShellAppT *shellGetApp(int32_t appId) { if (appId < 1 || appId >= SHELL_MAX_APPS) { return NULL; } if (sApps[appId].state == AppStateFreeE) { return NULL; } return &sApps[appId]; } // Load a DXE app: dlopen the module, resolve symbols, set up context, launch. // DXE3 is DJGPP's dynamic linking system — similar to dlopen/dlsym on Unix. // 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(); if (id < 0) { dvxMessageBox(ctx, "Error", "Maximum number of applications reached.", MB_OK | MB_ICONERROR); return -1; } // Check if this DXE is already loaded. If so, check whether the app // allows multiple instances. We read the descriptor from the existing // slot directly — no need to dlopen again. const char *loadPath = path; char tempPath[260] = {0}; ShellAppT *existing = findLoadedPath(path); if (existing) { // Read multiInstance from the already-loaded descriptor AppDescriptorT *existDesc = (AppDescriptorT *)dlsym(existing->dxeHandle, "_appDescriptor"); if (!existDesc || !existDesc->multiInstance) { 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(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; } // Look up required symbols AppDescriptorT *desc = (AppDescriptorT *)dlsym(handle, "_appDescriptor"); if (!desc) { char msg[256]; 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; } int32_t (*entry)(DxeAppContextT *) = (int32_t (*)(DxeAppContextT *))dlsym(handle, "_appMain"); if (!entry) { char msg[256]; 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; } void (*shutdown)(void) = (void (*)(void))dlsym(handle, "_appShutdown"); // Fill in the app slot ShellAppT *app = &sApps[id]; memset(app, 0, sizeof(*app)); 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; app->shutdownFn = shutdown; app->state = AppStateLoadedE; // Set up the context passed to appMain app->dxeCtx.shellCtx = ctx; app->dxeCtx.appId = id; // Derive app directory from path (everything up to the last separator). // This lets apps load resources relative to their own location rather // than the shell's working directory. snprintf(app->dxeCtx.appDir, sizeof(app->dxeCtx.appDir), "%s", path); char *sep = platformPathDirEnd(app->dxeCtx.appDir); if (sep) { *sep = '\0'; } else { app->dxeCtx.appDir[0] = '.'; app->dxeCtx.appDir[1] = '\0'; } // Launch. Set sCurrentAppId before any app code runs so that window // creation wrappers stamp the correct owner. Reset to 0 afterward so // shell-initiated operations (e.g., message boxes) aren't misattributed. sCurrentAppId = id; if (desc->hasMainLoop) { uint32_t stackSize = desc->stackSize > 0 ? (uint32_t)desc->stackSize : TS_DEFAULT_STACK_SIZE; int32_t priority = desc->priority; int32_t taskId = tsCreate(desc->name, appTaskWrapper, app, stackSize, priority); if (taskId < 0) { sCurrentAppId = 0; dvxMessageBox(ctx, "Error", "Failed to create task for application.", MB_OK | MB_ICONERROR); dlclose(handle); app->state = AppStateFreeE; return -1; } app->mainTaskId = (uint32_t)taskId; } else { // Callback-only: call entry directly in task 0 (the shell). // The app creates its windows and returns. From this point on, // the app lives entirely through event callbacks dispatched by // the shell's dvxUpdate loop. No separate task or stack needed. app->entryFn(&app->dxeCtx); } sCurrentAppId = 0; app->state = AppStateRunningE; shellLog("Shell: loaded '%s' (id=%ld, mainLoop=%s, entry=0x%08lx, desc=0x%08lx)", app->name, (long)id, app->hasMainLoop ? "yes" : "no", (unsigned long)entry, (unsigned long)desc); shellDesktopUpdate(); return id; } // Graceful reap — called from shellReapApps when an app has reached // the Terminating state. Unlike forceKill, this calls the app's // shutdown hook (if provided) giving it a chance to save state, close // files, etc. The sCurrentAppId is set during the shutdown call so // any final resource operations are attributed correctly. void shellReapApp(AppContextT *ctx, ShellAppT *app) { if (!app || app->state == AppStateFreeE) { return; } // Call shutdown hook if present if (app->shutdownFn) { sCurrentAppId = app->appId; app->shutdownFn(); sCurrentAppId = 0; } // Destroy all windows belonging to this app for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { if (ctx->stack.windows[i]->appId == app->appId) { dvxDestroyWindow(ctx, ctx->stack.windows[i]); } } // Kill the task if it has one and it's still alive if (app->hasMainLoop && app->mainTaskId > 0) { if (tsGetState(app->mainTaskId) != TaskStateTerminated) { tsKill(app->mainTaskId); } } // Close the DXE if (app->dxeHandle) { dlclose(app->dxeHandle); app->dxeHandle = NULL; } cleanupTempFile(app); shellLog("Shell: reaped app '%s'", app->name); app->state = AppStateFreeE; } // Called every frame from the shell's main loop. Scans for apps that have // entered the Terminating state (either their task returned or their last // window was closed) and cleans them up. The deferred-reap design avoids // destroying resources in the middle of a callback chain — the app marks // itself for termination, and cleanup happens at a safe top-level point. bool shellReapApps(AppContextT *ctx) { bool reaped = false; for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { if (sApps[i].state == AppStateTerminatingE) { shellReapApp(ctx, &sApps[i]); reaped = true; } } return reaped; } int32_t shellRunningAppCount(void) { int32_t count = 0; for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { if (sApps[i].state == AppStateRunningE || sApps[i].state == AppStateLoadedE) { count++; } } return count; } void shellTerminateAllApps(AppContextT *ctx) { for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { if (sApps[i].state != AppStateFreeE) { shellForceKillApp(ctx, &sApps[i]); } } }