371 lines
13 KiB
C
371 lines
13 KiB
C
// shellMain.c — DVX Shell entry point and main loop
|
|
//
|
|
// Initializes the GUI, task system, DXE export table, and loads
|
|
// the desktop app. Runs the cooperative main loop, yielding to
|
|
// app tasks and reaping terminated apps each frame.
|
|
//
|
|
// The main loop design (dvxUpdate + tsYield + reap + notify):
|
|
// Each iteration does four things:
|
|
// 1. dvxUpdate: processes input events, dispatches callbacks, composites
|
|
// dirty rects, flushes to the LFB. This is the shell's primary job.
|
|
// 2. tsYield: gives CPU time to app tasks. Without this, main-loop apps
|
|
// would never run because the shell task would monopolize the CPU.
|
|
// 3. shellReapApps: cleans up any apps that terminated during this frame
|
|
// (either their task returned or their last window was closed).
|
|
// 4. desktopUpdate: notifies the desktop app (Program Manager) if any
|
|
// apps were reaped, so it can refresh its task list.
|
|
//
|
|
// Crash recovery uses setjmp/longjmp:
|
|
// The shell installs signal handlers for SIGSEGV, SIGFPE, SIGILL. If a
|
|
// crash occurs in an app task, the handler longjmps back to the setjmp
|
|
// point in main(). This works because longjmp restores the main task's
|
|
// stack frame regardless of which task was running. tsRecoverToMain()
|
|
// then fixes the scheduler's bookkeeping, and the crashed app is killed.
|
|
// This gives the shell Windows 3.1-style fault tolerance — one bad app
|
|
// doesn't take down the whole system.
|
|
|
|
#include "shellApp.h"
|
|
#include "shellInfo.h"
|
|
#include "dvxDialog.h"
|
|
#include "platform/dvxPlatform.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <setjmp.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
// DJGPP-specific: provides __djgpp_exception_state_ptr for accessing
|
|
// CPU register state at the point of the exception
|
|
#include <sys/exceptn.h>
|
|
|
|
// ============================================================
|
|
// Module state
|
|
// ============================================================
|
|
|
|
static AppContextT sCtx;
|
|
// setjmp buffer for crash recovery. The crash handler longjmps here to
|
|
// return control to the shell's main loop after an app crashes.
|
|
static jmp_buf sCrashJmp;
|
|
// Volatile because it's written from a signal handler context. Tells
|
|
// the recovery code which signal fired (for logging/diagnostics).
|
|
static volatile int sCrashSignal = 0;
|
|
static FILE *sLogFile = NULL;
|
|
static void (*sDesktopUpdateFn)(void) = NULL;
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void crashHandler(int sig);
|
|
static void idleYield(void *ctx);
|
|
static void installCrashHandler(void);
|
|
static void logCrash(int sig);
|
|
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);
|
|
|
|
// ============================================================
|
|
// crashHandler — catch page faults and other fatal signals
|
|
// ============================================================
|
|
|
|
// Signal handler for fatal exceptions. DJGPP uses System V signal
|
|
// semantics where the handler is reset to SIG_DFL after each delivery,
|
|
// so we must re-install it before doing anything else.
|
|
//
|
|
// The longjmp is the key to crash recovery: it unwinds whatever stack
|
|
// we're on (potentially a crashed app's task stack) and restores the
|
|
// main task's stack frame to the setjmp point in main(). This is safe
|
|
// because cooperative switching means the main task's stack is always
|
|
// intact — it was cleanly suspended at a yield point. The crashed
|
|
// task's stack is abandoned (and later freed by tsKill).
|
|
static void crashHandler(int sig) {
|
|
logCrash(sig);
|
|
|
|
// Re-install handler (DJGPP resets to SIG_DFL after delivery)
|
|
signal(sig, crashHandler);
|
|
|
|
sCrashSignal = sig;
|
|
longjmp(sCrashJmp, 1);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// shellDesktopUpdate — notify desktop app of state change
|
|
// ============================================================
|
|
|
|
void shellDesktopUpdate(void) {
|
|
if (sDesktopUpdateFn) {
|
|
sDesktopUpdateFn();
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// idleYield — called when no dirty rects need compositing
|
|
// ============================================================
|
|
|
|
// Registered as sCtx.idleCallback. dvxUpdate calls this when it has
|
|
// processed all pending events and there are no dirty rects to composite.
|
|
// Instead of busy-spinning, we yield to app tasks — this is where most
|
|
// of the CPU time for main-loop apps comes from when the UI is idle.
|
|
// The tsActiveCount > 1 check avoids the overhead of a tsYield call
|
|
// (which would do a scheduler scan) when the shell is the only task.
|
|
static void idleYield(void *ctx) {
|
|
(void)ctx;
|
|
|
|
if (tsActiveCount() > 1) {
|
|
tsYield();
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// installCrashHandler
|
|
// ============================================================
|
|
|
|
static void installCrashHandler(void) {
|
|
signal(SIGSEGV, crashHandler);
|
|
signal(SIGFPE, crashHandler);
|
|
signal(SIGILL, crashHandler);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// logCrash — write exception details to the log
|
|
// ============================================================
|
|
|
|
// Dump as much diagnostic info as possible before longjmp destroys the
|
|
// crash context. This runs inside the signal handler, so only
|
|
// async-signal-safe functions should be used — but since we're in
|
|
// DJGPP (single-threaded DOS), reentrancy isn't a practical concern
|
|
// and vfprintf/fflush are safe to call here.
|
|
static void logCrash(int sig) {
|
|
const char *sigName = "UNKNOWN";
|
|
|
|
if (sig == SIGSEGV) {
|
|
sigName = "SIGSEGV (page fault)";
|
|
} else if (sig == SIGFPE) {
|
|
sigName = "SIGFPE (floating point exception)";
|
|
} else if (sig == SIGILL) {
|
|
sigName = "SIGILL (illegal instruction)";
|
|
}
|
|
|
|
shellLog("=== CRASH ===");
|
|
shellLog("Signal: %d (%s)", sig, sigName);
|
|
shellLog("Current app ID: %ld", (long)sCurrentAppId);
|
|
|
|
if (sCurrentAppId > 0) {
|
|
ShellAppT *app = shellGetApp(sCurrentAppId);
|
|
|
|
if (app) {
|
|
shellLog("App name: %s", app->name);
|
|
shellLog("App path: %s", app->path);
|
|
shellLog("Has main loop: %s", app->hasMainLoop ? "yes" : "no");
|
|
shellLog("Task ID: %lu", (unsigned long)app->mainTaskId);
|
|
}
|
|
} else {
|
|
shellLog("Crashed in shell (task 0)");
|
|
}
|
|
|
|
// __djgpp_exception_state_ptr is a DJGPP extension that captures the
|
|
// full CPU register state at the point of the exception. This gives
|
|
// us the faulting EIP, stack pointer, and all GPRs — invaluable for
|
|
// post-mortem debugging of app crashes from the log file.
|
|
jmp_buf *estate = __djgpp_exception_state_ptr;
|
|
|
|
if (estate) {
|
|
struct __jmp_buf *regs = &(*estate)[0];
|
|
shellLog("EIP: 0x%08lx CS: 0x%04x", regs->__eip, regs->__cs);
|
|
shellLog("EAX: 0x%08lx EBX: 0x%08lx ECX: 0x%08lx EDX: 0x%08lx", regs->__eax, regs->__ebx, regs->__ecx, regs->__edx);
|
|
shellLog("ESI: 0x%08lx EDI: 0x%08lx EBP: 0x%08lx ESP: 0x%08lx", regs->__esi, regs->__edi, regs->__ebp, regs->__esp);
|
|
shellLog("DS: 0x%04x ES: 0x%04x FS: 0x%04x GS: 0x%04x SS: 0x%04x", regs->__ds, regs->__es, regs->__fs, regs->__gs, regs->__ss);
|
|
shellLog("EFLAGS: 0x%08lx", regs->__eflags);
|
|
}
|
|
}
|
|
|
|
|
|
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) {
|
|
(void)userData;
|
|
shellLog(" %ldx%ld %ldbpp", (long)w, (long)h, (long)bpp);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// shellLog — append a line to DVX.LOG
|
|
// ============================================================
|
|
|
|
void shellLog(const char *fmt, ...) {
|
|
if (!sLogFile) {
|
|
return;
|
|
}
|
|
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vfprintf(sLogFile, fmt, ap);
|
|
va_end(ap);
|
|
|
|
fprintf(sLogFile, "\n");
|
|
fflush(sLogFile);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// shellRegisterDesktopUpdate
|
|
// ============================================================
|
|
|
|
void shellRegisterDesktopUpdate(void (*updateFn)(void)) {
|
|
sDesktopUpdateFn = updateFn;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// main
|
|
// ============================================================
|
|
|
|
int main(void) {
|
|
sLogFile = fopen("dvx.log", "w");
|
|
shellLog("DVX Shell starting...");
|
|
|
|
// Initialize GUI
|
|
int32_t result = dvxInit(&sCtx, 640, 480, 32);
|
|
|
|
if (result != 0) {
|
|
shellLog("Failed to initialize DVX GUI (error %ld)", (long)result);
|
|
|
|
if (sLogFile) {
|
|
fclose(sLogFile);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
shellLog("Available video modes:");
|
|
platformVideoEnumModes(logVideoMode, NULL);
|
|
shellLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch);
|
|
|
|
// Initialize task system
|
|
if (tsInit() != TS_OK) {
|
|
shellLog("Failed to initialize task system");
|
|
dvxShutdown(&sCtx);
|
|
|
|
if (sLogFile) {
|
|
fclose(sLogFile);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Shell task (task 0) gets high priority so the UI remains responsive
|
|
// even when app tasks are CPU-hungry. With HIGH priority (11 credits
|
|
// per epoch) vs app tasks at NORMAL (6 credits), the shell gets
|
|
// roughly twice as many scheduling turns as any single app.
|
|
tsSetPriority(0, TS_PRIORITY_HIGH);
|
|
|
|
// Gather system information (CPU, memory, drives, etc.)
|
|
shellInfoInit(&sCtx);
|
|
|
|
// Register DXE export table
|
|
shellExportInit();
|
|
|
|
// Initialize app slot table
|
|
shellAppInit();
|
|
|
|
// Set up idle callback for cooperative yielding. When dvxUpdate has
|
|
// no work to do (no input events, no dirty rects), it calls this
|
|
// instead of busy-looping. This is the main mechanism for giving
|
|
// app tasks CPU time during quiet periods.
|
|
sCtx.idleCallback = idleYield;
|
|
sCtx.idleCtx = &sCtx;
|
|
|
|
// Load the desktop app
|
|
int32_t desktopId = shellLoadApp(&sCtx, SHELL_DESKTOP_APP);
|
|
|
|
if (desktopId < 0) {
|
|
shellLog("Failed to load desktop app '%s'", SHELL_DESKTOP_APP);
|
|
tsShutdown();
|
|
dvxShutdown(&sCtx);
|
|
|
|
if (sLogFile) {
|
|
fclose(sLogFile);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Install crash handler after everything is initialized — if
|
|
// initialization itself crashes, we want the default DJGPP behavior
|
|
// (abort with register dump) rather than our recovery path, because
|
|
// the system isn't in a recoverable state yet.
|
|
installCrashHandler();
|
|
|
|
shellLog("DVX Shell ready.");
|
|
|
|
// Set recovery point for crash handler. setjmp returns 0 on initial
|
|
// call (falls through to the main loop). On a crash, longjmp makes
|
|
// setjmp return non-zero, entering this recovery block. The recovery
|
|
// code runs on the main task's stack (restored by longjmp) so it's
|
|
// safe to call any shell function.
|
|
if (setjmp(sCrashJmp) != 0) {
|
|
// Returned here from crash handler via longjmp.
|
|
// The task switcher's currentIdx still points to the crashed task.
|
|
// Fix it before doing anything else so the scheduler is consistent.
|
|
tsRecoverToMain();
|
|
|
|
shellLog("Recovering from crash, killing app %ld", (long)sCurrentAppId);
|
|
|
|
if (sCurrentAppId > 0) {
|
|
ShellAppT *app = shellGetApp(sCurrentAppId);
|
|
|
|
if (app) {
|
|
char msg[256];
|
|
snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name);
|
|
shellForceKillApp(&sCtx, app);
|
|
sCurrentAppId = 0;
|
|
dvxMessageBox(&sCtx, "Application Error", msg, MB_OK | MB_ICONERROR);
|
|
}
|
|
}
|
|
|
|
sCurrentAppId = 0;
|
|
sCrashSignal = 0;
|
|
shellDesktopUpdate();
|
|
}
|
|
|
|
// Main loop — runs until dvxQuit() sets sCtx.running = false.
|
|
// Two yield points per iteration: one explicit (below) and one via
|
|
// the idle callback inside dvxUpdate. The explicit yield here ensures
|
|
// app tasks get CPU time even during busy frames (lots of repaints).
|
|
// Without it, a flurry of mouse-move events could starve app tasks
|
|
// because dvxUpdate would keep finding work to do and never call idle.
|
|
while (sCtx.running) {
|
|
dvxUpdate(&sCtx);
|
|
|
|
// Give app tasks CPU time even during active frames
|
|
if (tsActiveCount() > 1) {
|
|
tsYield();
|
|
}
|
|
|
|
// Reap terminated apps and notify desktop if anything changed.
|
|
// This is the safe point for cleanup — we're at the top of the
|
|
// main loop, not inside any callback or compositor operation.
|
|
if (shellReapApps(&sCtx)) {
|
|
shellDesktopUpdate();
|
|
}
|
|
}
|
|
|
|
shellLog("DVX Shell shutting down...");
|
|
|
|
// Clean shutdown: terminate all apps first (destroys windows, kills
|
|
// tasks, closes DXE handles), then tear down the task system and GUI
|
|
// in reverse initialization order.
|
|
shellTerminateAllApps(&sCtx);
|
|
|
|
tsShutdown();
|
|
dvxShutdown(&sCtx);
|
|
|
|
shellLog("DVX Shell exited.");
|
|
|
|
if (sLogFile) {
|
|
fclose(sLogFile);
|
|
sLogFile = NULL;
|
|
}
|
|
|
|
return 0;
|
|
}
|