Multi-instance app support added.

This commit is contained in:
Scott Duensing 2026-03-18 01:25:05 -05:00
parent 84b0762820
commit fc3a513ada
3 changed files with 166 additions and 5 deletions

View file

@ -66,6 +66,7 @@ static void updateTime(void);
AppDescriptorT appDescriptor = { AppDescriptorT appDescriptor = {
.name = "Clock", .name = "Clock",
.hasMainLoop = true, .hasMainLoop = true,
.multiInstance = true,
.stackSize = 0, .stackSize = 0,
.priority = TS_PRIORITY_LOW .priority = TS_PRIORITY_LOW
}; };

View file

@ -30,6 +30,10 @@ int32_t sCurrentAppId = 0;
static int32_t allocSlot(void); static int32_t allocSlot(void);
static void appTaskWrapper(void *arg); static void appTaskWrapper(void *arg);
static const char *baseName(const char *path); 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 shellAppInit(void);
void shellForceKillApp(AppContextT *ctx, ShellAppT *app); void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
ShellAppT *shellGetApp(int32_t appId); ShellAppT *shellGetApp(int32_t appId);
@ -88,6 +92,96 @@ static const char *baseName(const char *path) {
return slash ? slash + 1 : 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) // Public API (alphabetical)
// ============================================================ // ============================================================
@ -129,6 +223,7 @@ void shellForceKillApp(AppContextT *ctx, ShellAppT *app) {
app->dxeHandle = NULL; app->dxeHandle = NULL;
} }
cleanupTempFile(app);
app->state = AppStateFreeE; app->state = AppStateFreeE;
shellLog("Shell: force-killed app '%s'", app->name); 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 // Each .app file is a DXE3 shared object that exports _appDescriptor and
// _appMain (and optionally _appShutdown). The leading underscore is the // _appMain (and optionally _appShutdown). The leading underscore is the
// COFF symbol convention; DJGPP's dlsym expects it. // 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) { int32_t shellLoadApp(AppContextT *ctx, const char *path) {
// Allocate a slot // Allocate a slot
int32_t id = allocSlot(); int32_t id = allocSlot();
@ -161,13 +264,56 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
return -1; 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 // Load the DXE
void *handle = dlopen(path, RTLD_GLOBAL); void *handle = dlopen(loadPath, RTLD_GLOBAL);
if (!handle) { if (!handle) {
char msg[512]; char msg[512];
snprintf(msg, sizeof(msg), "Failed to load %s:\n%s", baseName(path), dlerror()); snprintf(msg, sizeof(msg), "Failed to load %s:\n%s", baseName(path), dlerror());
dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR);
if (tempPath[0]) {
remove(tempPath);
}
return -1; return -1;
} }
@ -179,6 +325,11 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
snprintf(msg, sizeof(msg), "%s: missing appDescriptor", baseName(path)); snprintf(msg, sizeof(msg), "%s: missing appDescriptor", baseName(path));
dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR);
dlclose(handle); dlclose(handle);
if (tempPath[0]) {
remove(tempPath);
}
return -1; return -1;
} }
@ -189,6 +340,11 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
snprintf(msg, sizeof(msg), "%s: missing appMain", baseName(path)); snprintf(msg, sizeof(msg), "%s: missing appMain", baseName(path));
dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR);
dlclose(handle); dlclose(handle);
if (tempPath[0]) {
remove(tempPath);
}
return -1; return -1;
} }
@ -201,6 +357,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
app->appId = id; app->appId = id;
snprintf(app->name, SHELL_APP_NAME_MAX, "%s", desc->name); snprintf(app->name, SHELL_APP_NAME_MAX, "%s", desc->name);
snprintf(app->path, sizeof(app->path), "%s", path); snprintf(app->path, sizeof(app->path), "%s", path);
snprintf(app->tempPath, sizeof(app->tempPath), "%s", tempPath);
app->dxeHandle = handle; app->dxeHandle = handle;
app->hasMainLoop = desc->hasMainLoop; app->hasMainLoop = desc->hasMainLoop;
app->entryFn = entry; app->entryFn = entry;
@ -304,6 +461,7 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) {
app->dxeHandle = NULL; app->dxeHandle = NULL;
} }
cleanupTempFile(app);
shellLog("Shell: reaped app '%s'", app->name); shellLog("Shell: reaped app '%s'", app->name);
app->state = AppStateFreeE; app->state = AppStateFreeE;
} }

View file

@ -44,6 +44,7 @@
typedef struct { typedef struct {
char name[SHELL_APP_NAME_MAX]; char name[SHELL_APP_NAME_MAX];
bool hasMainLoop; bool hasMainLoop;
bool multiInstance; // true = allow multiple instances via temp copy
int32_t stackSize; // 0 = TS_DEFAULT_STACK_SIZE int32_t stackSize; // 0 = TS_DEFAULT_STACK_SIZE
int32_t priority; // TS_PRIORITY_* or custom int32_t priority; // TS_PRIORITY_* or custom
} AppDescriptorT; } AppDescriptorT;
@ -84,6 +85,7 @@ typedef struct {
int32_t appId; // unique ID = slot index (1-based; 0 = shell) int32_t appId; // unique ID = slot index (1-based; 0 = shell)
char name[SHELL_APP_NAME_MAX]; char name[SHELL_APP_NAME_MAX];
char path[260]; char path[260];
char tempPath[260]; // temp copy path for multi-instance (empty = not a copy)
void *dxeHandle; // dlopen() handle void *dxeHandle; // dlopen() handle
AppStateE state; AppStateE state;
bool hasMainLoop; bool hasMainLoop;