387 lines
17 KiB
Markdown
387 lines
17 KiB
Markdown
# DVX Shell
|
|
|
|
The DVX Shell (`dvxshell.lib`) is a DXE module loaded by the DVX loader at
|
|
startup. It initializes the GUI subsystem, loads DXE3 application modules at
|
|
runtime, runs the cooperative main loop, and provides crash recovery so a
|
|
faulting app does not take down the entire system.
|
|
|
|
The shell is not a standalone executable. The loader maps it into memory via
|
|
`dlopen`, resolves its entry point (`shellMain`), and calls it. All symbols
|
|
the shell needs -- libdvx, libtasks, widgets, libc -- are resolved at load
|
|
time from DXE modules already loaded by the loader and the loader's own
|
|
`dlregsym` table.
|
|
|
|
## Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `shellMain.c` | Entry point (`shellMain`), main loop, crash recovery, logging |
|
|
| `shellApp.c` | App loading via `dlopen`/`dlsym`, lifecycle, reaping |
|
|
| `shellApp.h` | `ShellAppT`, `AppDescriptorT`, `AppStateE`, `DxeAppContextT`, API |
|
|
| `shellExport.c` | 3 DXE wrapper overrides for window resource tracking |
|
|
| `shellInfo.c` | System info gathering |
|
|
| `shellInfo.h` | `shellInfoInit()`, `shellGetSystemInfo()` |
|
|
| `shellTaskMgr.c` | Task Manager window |
|
|
| `shellTaskMgr.h` | `shellTaskMgrOpen()`, `shellTaskMgrRefresh()` |
|
|
| `Makefile` | Cross-compile rules, produces `dvxshell.lib` and `dvxshell.dep` |
|
|
|
|
## Building
|
|
|
|
```
|
|
make # builds ../bin/libs/dvxshell.lib and ../bin/libs/dvxshell.dep
|
|
make clean # removes objects, binary, and deployed config files
|
|
```
|
|
|
|
CFLAGS: `-O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../tasks -I../tasks/thirdparty`
|
|
|
|
The shell is compiled to object files and packaged into a DXE via `dxe3gen`.
|
|
It exports `_shell` so the loader can resolve `shellMain`. All other symbols
|
|
are unresolved imports (`-U`) satisfied at load time.
|
|
|
|
Requires the DJGPP cross-compiler toolchain and the DXE3 tools (`dxe3gen`).
|
|
|
|
## Dependencies
|
|
|
|
`dvxshell.dep` lists the modules the loader must load before the shell:
|
|
|
|
```
|
|
libtasks
|
|
libdvx
|
|
box
|
|
button
|
|
checkbox
|
|
dropdown
|
|
label
|
|
listbox
|
|
listview
|
|
radio
|
|
separator
|
|
spacer
|
|
statbar
|
|
textinpt
|
|
```
|
|
|
|
The loader reads this file, loads each module via `dlopen` with `RTLD_GLOBAL`,
|
|
then loads the shell itself. This is how all symbols (dvx*, wgt*, ts*, etc.)
|
|
become available without the shell statically linking anything.
|
|
|
|
## Startup Sequence
|
|
|
|
`shellMain()` in `shellMain.c` is called by the loader after all dependencies
|
|
are loaded. It performs initialization in this order:
|
|
|
|
1. **Truncate log** -- open `dvx.log` for write to clear it, then close.
|
|
All subsequent writes use append-per-write (the file is never held open).
|
|
2. **Load preferences** -- `prefsLoad("CONFIG/DVX.INI")`. Missing file or
|
|
keys silently fall back to compiled-in defaults.
|
|
3. **dvxInit** -- initialize VESA video (LFB), backbuffer, compositor, window
|
|
manager, font, cursor, input subsystems. Reads video width/height/bpp from
|
|
preferences (default 640x480x16).
|
|
4. **Mouse config** -- read wheel direction, double-click speed, acceleration
|
|
from `[mouse]` section and call `dvxSetMouseConfig()`.
|
|
5. **Color scheme** -- read `[colors]` section (20 RGB triplets), apply via
|
|
`dvxApplyColorScheme()`.
|
|
6. **Wallpaper** -- read `[desktop]` section for wallpaper path and mode
|
|
(stretch/tile/center), load via `dvxSetWallpaper()`.
|
|
7. **Video mode log** -- enumerate all available VESA modes to `dvx.log`.
|
|
8. **Task system** -- `tsInit()`, set shell task (task 0) to
|
|
`TS_PRIORITY_HIGH` so the UI stays responsive under load.
|
|
9. **System info** -- `shellInfoInit()` gathers CPU, memory, drive info via
|
|
the platform layer and logs it.
|
|
10. **DXE exports** -- `shellExportInit()` calls `dlregsym()` to register the
|
|
3 wrapper overrides. Must happen before any `dlopen()` of app DXEs.
|
|
11. **App slot table** -- `shellAppInit()` zeroes the 32-slot fixed array.
|
|
12. **Idle/hotkey callbacks** -- wire up `idleYield`, `ctrlEscHandler`,
|
|
`titleChangeHandler` on the `AppContextT`.
|
|
13. **Desktop app** -- `shellLoadApp(ctx, "apps/progman/progman.app")`. If
|
|
this fails, the shell exits.
|
|
14. **Crash handlers** -- `installCrashHandler()` registers signal handlers
|
|
for SIGSEGV, SIGFPE, SIGILL. Installed last so initialization crashes
|
|
get the default DJGPP abort-with-register-dump instead of our recovery
|
|
path.
|
|
|
|
## Main Loop
|
|
|
|
The main loop runs until `dvxQuit()` sets `ctx->running = false`:
|
|
|
|
```
|
|
while (ctx->running) {
|
|
dvxUpdate(ctx); // (1)
|
|
tsYield(); // (2)
|
|
shellReapApps(ctx); // (3)
|
|
shellDesktopUpdate(); // (4)
|
|
}
|
|
```
|
|
|
|
1. **dvxUpdate** -- processes mouse/keyboard input, dispatches widget and
|
|
window callbacks, composites dirty rects, flushes to the LFB. This is the
|
|
shell's primary job. When idle (no events, no dirty rects), dvxUpdate
|
|
calls the registered `idleCallback` which yields to app tasks.
|
|
2. **tsYield** -- explicit yield to give app tasks CPU time even during busy
|
|
frames with many repaints. Without this, a flurry of mouse-move events
|
|
could starve app tasks because dvxUpdate would keep finding work.
|
|
3. **shellReapApps** -- scans the 32-slot app table for apps in
|
|
`AppStateTerminatingE`. Calls the shutdown hook (if present), destroys
|
|
all windows owned by the app, kills the task, closes the DXE handle,
|
|
and cleans up temp files. Returns true if anything was reaped.
|
|
4. **shellDesktopUpdate** -- if apps were reaped, iterates the registered
|
|
desktop update callback list so listeners (Program Manager, Task Manager)
|
|
can refresh their UI.
|
|
|
|
## Crash Recovery
|
|
|
|
The shell provides Windows 3.1-style fault tolerance using `setjmp`/`longjmp`:
|
|
|
|
1. `installCrashHandler()` registers `crashHandler` for SIGSEGV, SIGFPE,
|
|
SIGILL.
|
|
2. `setjmp(sCrashJmp)` in `shellMain()` establishes the recovery point.
|
|
3. If a signal fires (in any task), `crashHandler` logs the crash details
|
|
(signal, app name, full register dump from `__djgpp_exception_state_ptr`),
|
|
re-installs the handler (DJGPP uses SysV semantics -- handler resets to
|
|
`SIG_DFL` after each delivery), then `longjmp(sCrashJmp, 1)`.
|
|
4. `longjmp` restores the main task's stack frame to the `setjmp` point.
|
|
This is safe because cooperative switching means the main task's stack
|
|
is always intact -- it was cleanly suspended at a yield point.
|
|
5. `tsRecoverToMain()` fixes the task scheduler's `currentIdx` so it points
|
|
back to task 0 instead of the crashed task.
|
|
6. The crashed app is force-killed via `shellForceKillApp()`, and a message
|
|
box is displayed: "'AppName' has caused a fault and will be terminated."
|
|
7. The desktop update callbacks are notified so the UI refreshes.
|
|
|
|
The register dump logged includes EIP, CS, all GPRs (EAX-EDI), segment
|
|
registers, and EFLAGS -- invaluable for post-mortem debugging.
|
|
|
|
## App Lifecycle
|
|
|
|
### DXE3 Loading
|
|
|
|
DXE3 is DJGPP's dynamic linking mechanism. Each `.app` file is a DXE3 shared
|
|
object. Symbols are resolved from the loaded RTLD_GLOBAL modules (libdvx,
|
|
libtasks, widgets) and the shell's 3 wrapper overrides registered via
|
|
`dlregsym()`. The load sequence in `shellLoadApp()`:
|
|
|
|
1. Allocate a slot from the 32-entry fixed array (slot 0 is the shell).
|
|
2. Check if this DXE path is already loaded (`findLoadedPath`).
|
|
- If the existing app's descriptor says `multiInstance = true`, copy
|
|
the `.app` to a unique temp file (using `TEMP`/`TMP` env var) so
|
|
`dlopen` gets an independent code+data image.
|
|
- If `multiInstance = false`, block with an error message.
|
|
3. `dlopen(loadPath, RTLD_GLOBAL)` -- load the DXE.
|
|
4. `dlsym(handle, "_appDescriptor")` -- resolve the metadata struct.
|
|
5. `dlsym(handle, "_appMain")` -- resolve the entry point.
|
|
6. `dlsym(handle, "_appShutdown")` -- resolve the optional shutdown hook.
|
|
7. Fill in the `ShellAppT` slot (name, path, handle, state, etc.).
|
|
8. Derive `appDir` (directory containing the `.app` file) and `configDir`
|
|
(`CONFIG/<apppath>/` -- mirrors the app's directory structure under
|
|
`CONFIG/`).
|
|
9. Set `sCurrentAppId` to the slot index.
|
|
10. Launch:
|
|
- **Callback-only** (`hasMainLoop = false`): call `entryFn()` directly
|
|
in task 0. The app creates windows, registers callbacks, and returns.
|
|
- **Main-loop** (`hasMainLoop = true`): call `tsCreate()` to make a
|
|
cooperative task with the descriptor's stack size and priority. The
|
|
task wrapper sets `sCurrentAppId`, calls `entryFn()`, and marks the
|
|
app `AppStateTerminatingE` when it returns.
|
|
11. Reset `sCurrentAppId` to 0. Set state to `AppStateRunningE`. Notify
|
|
desktop update callbacks.
|
|
|
|
### AppDescriptorT
|
|
|
|
Every DXE app exports a global `AppDescriptorT`:
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `name` | `char[64]` | Display name (Task Manager, title bars) |
|
|
| `hasMainLoop` | `bool` | `true` = dedicated cooperative task; `false` = callback-only |
|
|
| `multiInstance` | `bool` | `true` = allow multiple simultaneous instances via temp file copy |
|
|
| `stackSize` | `int32_t` | Task stack in bytes (`SHELL_STACK_DEFAULT` = use default) |
|
|
| `priority` | `int32_t` | `TS_PRIORITY_LOW` / `TS_PRIORITY_NORMAL` / `TS_PRIORITY_HIGH` |
|
|
|
|
### Callback vs Main-Loop Apps
|
|
|
|
**Callback-only** (`hasMainLoop = false`):
|
|
- `appMain()` runs in the shell's task 0, creates windows, registers event
|
|
callbacks, and returns immediately.
|
|
- All subsequent work happens through callbacks dispatched by `dvxUpdate()`.
|
|
- Lifecycle ends when the last window is closed (detected by the
|
|
`shellWrapDestroyWindow` wrapper).
|
|
- No task stack allocated -- simpler and cheaper.
|
|
- Examples: Program Manager, Notepad, Control Panel, DVX Demo, Image Viewer.
|
|
|
|
**Main-loop** (`hasMainLoop = true`):
|
|
- A cooperative task is created via `tsCreate()`.
|
|
- `appMain()` runs in that task with its own loop calling `tsYield()`.
|
|
- Needed when the app has ongoing work that cannot be expressed purely as
|
|
event callbacks (polling, computation, animation).
|
|
- Lifecycle ends when `appMain()` returns.
|
|
- Example: Clock (polls `time()` each second).
|
|
|
|
### Multi-Instance Support
|
|
|
|
DXE3's `dlopen` is reference-counted per path -- loading the same `.app`
|
|
twice returns the same handle, sharing all globals and statics. For apps
|
|
that set `multiInstance = true`, the shell copies the `.app` to a temp file
|
|
(e.g., `C:\TEMP\_dvx02.app`) before `dlopen`, giving each instance its own
|
|
code and data segment. Temp files are cleaned up on app termination.
|
|
|
|
### App State Machine
|
|
|
|
```
|
|
Free -> Loaded -> Running -> Terminating -> Free
|
|
```
|
|
|
|
- **Free**: slot available.
|
|
- **Loaded**: DXE loaded, entry point not yet called (transient).
|
|
- **Running**: entry point called, app is active.
|
|
- **Terminating**: app's task returned or last window closed; awaiting reap.
|
|
|
|
## Resource Tracking
|
|
|
|
`sCurrentAppId` is a global set before calling any app code (entry, shutdown,
|
|
callbacks). The shell's `dvxCreateWindow` wrapper stamps every new window with
|
|
`win->appId = sCurrentAppId`. This is how the shell knows which windows belong
|
|
to which app, enabling:
|
|
|
|
- Per-app window cleanup on crash or termination (walk the window stack,
|
|
destroy all windows with matching `appId`).
|
|
- The last-window-closes-app rule for callback-only apps: when
|
|
`shellWrapDestroyWindow` detects that a callback-only app has no remaining
|
|
windows, it marks the app `AppStateTerminatingE`.
|
|
|
|
`sCurrentAppId` is a simple global (not thread-local) because cooperative
|
|
multitasking means only one task runs at a time.
|
|
|
|
## DXE Export Table
|
|
|
|
`shellExport.c` registers 3 wrapper functions via `dlregsym()` that override
|
|
the real implementations for subsequently loaded app DXEs:
|
|
|
|
1. `dvxCreateWindow` -- stamps `win->appId` for resource ownership tracking.
|
|
2. `dvxCreateWindowCentered` -- stamps `win->appId` for resource ownership
|
|
tracking.
|
|
3. `dvxDestroyWindow` -- checks if the destroyed window was a callback-only
|
|
app's last window and marks it for reaping.
|
|
|
|
The key mechanic: `dlregsym` takes precedence over `RTLD_GLOBAL` exports.
|
|
Since libdvx (which has the real functions) was loaded before
|
|
`shellExportInit()` registers these wrappers, libdvx keeps the real
|
|
implementations. But any app DXE loaded afterward gets the wrappers, which
|
|
add resource tracking transparently.
|
|
|
|
All other symbol exports -- dvx*, wgt*, ts*, platform*, libc, libm -- come
|
|
from the DXE modules loaded with `RTLD_GLOBAL` by the loader. They no longer
|
|
need to be listed in the shell's export table.
|
|
|
|
## Task Manager
|
|
|
|
`shellTaskMgr.c` implements a shell-level Task Manager accessible via
|
|
Ctrl+Esc regardless of which app is focused or whether the desktop app
|
|
is running. It is owned by the shell (appId = 0), not by any DXE app.
|
|
|
|
Features:
|
|
- **ListView** with 5 columns: Name, Title, File, Type (Task/Callback), Status.
|
|
- **Switch To** -- find the app's topmost window, restore if minimized, raise
|
|
and focus it.
|
|
- **End Task** -- force-kill the selected app via `shellForceKillApp()`.
|
|
- **Run...** -- open a file dialog filtered to `*.app`, load the selected file.
|
|
- **Status bar** -- shows running app count and memory usage (total/free MB).
|
|
- Registers with `shellRegisterDesktopUpdate` so the list auto-refreshes when
|
|
apps load, terminate, or crash.
|
|
|
|
## Desktop Update Callbacks
|
|
|
|
`shellRegisterDesktopUpdate(fn)` adds a function pointer to a dynamic array
|
|
(managed via stb_ds `arrput`/`arrdel`). `shellDesktopUpdate()` iterates the
|
|
array and calls each registered function.
|
|
|
|
This is the mechanism by which the shell notifies interested parties (Program
|
|
Manager status bar, Task Manager list) when app state changes -- without
|
|
polling. Multiple listeners are supported. Listeners should call
|
|
`shellUnregisterDesktopUpdate()` before they are destroyed.
|
|
|
|
## App Config Storage
|
|
|
|
Each app gets a dedicated writable config directory under `CONFIG/`. The path
|
|
mirrors the app's location under `APPS/`:
|
|
|
|
```
|
|
App path: APPS/PROGMAN/progman.app
|
|
Config dir: CONFIG/PROGMAN/
|
|
```
|
|
|
|
API:
|
|
- `shellEnsureConfigDir(ctx)` -- create the directory tree via
|
|
`platformMkdirRecursive()`. Returns 0 on success.
|
|
- `shellConfigPath(ctx, "settings.ini", buf, sizeof(buf))` -- build a full
|
|
path to a file in the config directory.
|
|
|
|
Apps use the standard preferences system (`prefsLoad`/`prefsSave`) pointed at
|
|
their config directory for persistent settings.
|
|
|
|
## System Hotkeys
|
|
|
|
These are always active regardless of which app is focused:
|
|
|
|
| Hotkey | Action |
|
|
|--------|--------|
|
|
| Alt+Tab | Cycle windows forward (rotate top to bottom of stack) |
|
|
| Shift+Alt+Tab | Cycle windows backward (pull bottom to top) |
|
|
| Alt+F4 | Close the focused window (calls its onClose callback) |
|
|
| Ctrl+F12 | Full screen screenshot -- prompts for save path (PNG/BMP/JPG) |
|
|
| Ctrl+Shift+F12 | Focused window screenshot -- prompts for save path |
|
|
| Ctrl+Esc | Open/raise the Task Manager (shell-level, always available) |
|
|
| F10 | Activate/toggle the focused window's menu bar |
|
|
| Alt+Space | Open/close the system menu on the focused window |
|
|
|
|
## Screenshot System
|
|
|
|
Two screenshot functions are available, both accessible from the system menu
|
|
(Alt+Space) or via hotkeys:
|
|
|
|
- `interactiveScreenshot(ctx)` -- captures the full screen (composited
|
|
backbuffer), then opens a Save As file dialog filtered to PNG/BMP/JPG/TGA.
|
|
Called by Ctrl+F12 or the "Screenshot..." system menu item.
|
|
- `interactiveWindowScreenshot(ctx, win)` -- captures just the focused
|
|
window's content buffer. Called by Ctrl+Shift+F12 or the "Window Shot..."
|
|
system menu item.
|
|
|
|
The system menu also includes standard window operations: Restore, Move, Size,
|
|
Minimize, Maximize, and Close.
|
|
|
|
## Logging
|
|
|
|
`shellLog(fmt, ...)` appends a printf-formatted line to `dvx.log`. The file
|
|
is opened, written, and closed on each call (append-per-write) so:
|
|
- The file is never held open, allowing Notepad to read it while the shell
|
|
runs.
|
|
- Writes are flushed immediately, important for crash diagnostics.
|
|
- The file is truncated once at startup.
|
|
|
|
Log output includes: startup sequence, preferences applied, video modes
|
|
enumerated, system information, app load/reap events, crash details with
|
|
full register dumps.
|
|
|
|
## Shell API Summary
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `shellMain(argc, argv)` | Entry point called by the DVX loader |
|
|
| `shellAppInit()` | Zero the 32-slot app table |
|
|
| `shellLoadApp(ctx, path)` | Load and start a DXE app. Returns app ID (>= 1) or -1 |
|
|
| `shellReapApps(ctx)` | Clean up terminated apps (call each frame). Returns true if any reaped |
|
|
| `shellReapApp(ctx, app)` | Gracefully shut down one app (calls shutdown hook) |
|
|
| `shellForceKillApp(ctx, app)` | Forcibly kill an app -- skips shutdown hook |
|
|
| `shellTerminateAllApps(ctx)` | Kill all running apps (shell shutdown) |
|
|
| `shellGetApp(appId)` | Get app slot by ID (NULL if invalid/free) |
|
|
| `shellRunningAppCount()` | Count running apps (excluding the shell) |
|
|
| `shellLog(fmt, ...)` | Append to dvx.log |
|
|
| `shellEnsureConfigDir(ctx)` | Create an app's config directory tree |
|
|
| `shellConfigPath(ctx, name, buf, size)` | Build path to file in app's config dir |
|
|
| `shellExportInit()` | Register 3 DXE wrapper overrides via dlregsym() |
|
|
| `shellRegisterDesktopUpdate(fn)` | Register callback for app state changes |
|
|
| `shellUnregisterDesktopUpdate(fn)` | Remove a previously registered callback |
|
|
| `shellDesktopUpdate()` | Notify all registered listeners |
|
|
| `shellTaskMgrOpen(ctx)` | Open or raise the Task Manager window |
|
|
| `shellTaskMgrRefresh()` | Refresh the Task Manager list and status |
|
|
| `shellInfoInit(ctx)` | Gather and log system hardware information |
|
|
| `shellGetSystemInfo()` | Return cached system info text |
|