DVX_GUI/dvxshell/shellApp.c

350 lines
11 KiB
C

// 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 <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
// ============================================================
// 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);
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;
}
// ============================================================
// 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;
}
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.
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;
}
// Load the DXE
void *handle = dlopen(path, 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);
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);
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);
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);
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 last '/' or '\').
// This lets apps load resources relative to their own location rather
// than the shell's working directory. Handles both Unix and DOS path
// separators because DJGPP accepts either.
snprintf(app->dxeCtx.appDir, sizeof(app->dxeCtx.appDir), "%s", path);
char *lastSlash = strrchr(app->dxeCtx.appDir, '/');
char *lastBack = strrchr(app->dxeCtx.appDir, '\\');
if (lastBack > lastSlash) {
lastSlash = lastBack;
}
if (lastSlash) {
*lastSlash = '\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);
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;
}
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]);
}
}
}