mirror of
https://github.com/felixrieseberg/windows95.git
synced 2026-05-09 00:24:09 +00:00
Shared text clipboard via VMware backdoor + W95TOOLS.EXE guest agent (#361)
Three layers: v86 — src/vmware.js gains the legacy text-clipboard backdoor commands (GETSELLENGTH/GETNEXTPIECE/SETSELLENGTH/SETNEXTPIECE, 6–9). The host stages bytes via the vmware-clipboard-host bus event; the guest pushes via 8/9 and the device emits vmware-clipboard-guest when the buffer fills. Same wire protocol as open-vm-tools' pre-RPC copy/paste. Committed on the windows95-base fork branch; libv86.js rebuilt here. renderer — src/renderer/clipboard.ts polls Electron's clipboard (no change event exists), translates host UTF-8/LF ↔ guest CP-1252/CRLF, and bounces bytes through the two bus events. Echo-suppressed so a value we just wrote does not come back as a change. guest — guest-tools/agent/W95TOOLS.EXE is a 22 KB hidden-window agent that joins the Win32 clipboard-viewer chain (push-on-copy) and polls the backdoor on a 250 ms timer (pull-from-host). Win9x runs ring-3 with the I/O bitmap wide open, so a plain IN EAX,DX from a user process reaches the port — no driver needed. Named for growth: time sync and host-initiated shutdown will live here too. Built with Open Watcom v2 inside Docker (Makefile + Dockerfile alongside the source); subsystem 4.0, no msvcrt, runs on Win95 RTM. Install: copy \\HOST\TOOLS\agent\W95TOOLS.EXE into the guest and drop a shortcut in StartUp. Text only, 64 KB cap.
This commit is contained in:
@@ -21,3 +21,23 @@ Install inside the guest:
|
||||
absolute mouse driver**.
|
||||
3. Reboot. The app detects the driver and stops grabbing pointer lock;
|
||||
ESC still toggles lock for games that want raw relative input.
|
||||
|
||||
## agent/ — W95TOOLS guest agent
|
||||
|
||||
`W95TOOLS.EXE` is a hidden-window agent that talks to the emulator over
|
||||
the VMware backdoor (port 0x5658). Currently it does one thing: bridges
|
||||
Windows 95's `CF_TEXT` clipboard to the host (legacy backdoor commands
|
||||
6–9; host side is `src/renderer/clipboard.ts`, which polls Electron's
|
||||
clipboard). It's also where time sync, host-initiated shutdown, and a
|
||||
tray icon will live when those land.
|
||||
|
||||
Install inside the guest:
|
||||
|
||||
1. Copy `\\HOST\TOOLS\agent\W95TOOLS.EXE` to `C:\WINDOWS\`.
|
||||
2. Drop a shortcut to it in
|
||||
`C:\WINDOWS\Start Menu\Programs\StartUp` so it runs on login.
|
||||
|
||||
Copy text on either side and it appears on the other within ~250 ms.
|
||||
Text only; conversion is Windows-1252 ↔ UTF-8 with CRLF ↔ LF, capped at
|
||||
64 KB. Built from `w95tools.c` with Open Watcom v2 — `make -C
|
||||
guest-tools/agent` (needs Docker).
|
||||
|
||||
12
guest-tools/agent/Dockerfile
Normal file
12
guest-tools/agent/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM --platform=linux/amd64 debian:bookworm-slim
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends curl xz-utils ca-certificates make && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN mkdir -p /opt/watcom && \
|
||||
curl -fsSL https://github.com/open-watcom/open-watcom-v2/releases/download/Current-build/ow-snapshot.tar.xz \
|
||||
| tar -xJ -C /opt/watcom
|
||||
ENV WATCOM=/opt/watcom
|
||||
ENV PATH=$WATCOM/binl64:$PATH
|
||||
ENV INCLUDE=$WATCOM/h:$WATCOM/h/nt
|
||||
ENV LIB=$WATCOM/lib386:$WATCOM/lib386/nt
|
||||
WORKDIR /work
|
||||
30
guest-tools/agent/Makefile
Normal file
30
guest-tools/agent/Makefile
Normal file
@@ -0,0 +1,30 @@
|
||||
# Build W95TOOLS.EXE for Windows 95 with Open Watcom v2, inside Docker.
|
||||
#
|
||||
# Watcom is the only readily-available cross-compiler that emits a PE binary
|
||||
# Win95 RTM will load (subsystem 4.0, no msvcrt). mingw-w64 targets NT. The
|
||||
# macOS-native Watcom binaries are unsigned and Gatekeeper kills them, so we
|
||||
# run the linux/amd64 build under Docker instead.
|
||||
|
||||
IMAGE := windows95-ow2
|
||||
# Docker Desktop's bind-mount file sync races with recently-edited files; work
|
||||
# around it by piping the source on stdin and building in /tmp inside the
|
||||
# container, then dumping the EXE bytes back over stdout.
|
||||
DOCKER := docker run --rm -i --platform linux/amd64 $(IMAGE)
|
||||
CFLAGS := -bt=nt -3r -zq -wx -we -os -s
|
||||
|
||||
.PHONY: all image clean
|
||||
|
||||
all: W95TOOLS.EXE
|
||||
|
||||
image:
|
||||
docker build --platform linux/amd64 -t $(IMAGE) .
|
||||
|
||||
W95TOOLS.EXE: w95tools.c image
|
||||
$(DOCKER) sh -c 'cd /tmp && cat >w95tools.c && \
|
||||
wcc386 $(CFLAGS) w95tools.c && \
|
||||
wlink system nt_win option quiet name W95TOOLS.EXE \
|
||||
file w95tools.o library kernel32,user32 && \
|
||||
cat W95TOOLS.EXE' <w95tools.c >$@
|
||||
|
||||
clean:
|
||||
rm -f W95TOOLS.EXE
|
||||
BIN
guest-tools/agent/W95TOOLS.EXE
Normal file
BIN
guest-tools/agent/W95TOOLS.EXE
Normal file
Binary file not shown.
181
guest-tools/agent/w95tools.c
Normal file
181
guest-tools/agent/w95tools.c
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* W95TOOLS — guest-side integration agent for the windows95 emulator.
|
||||
*
|
||||
* Currently: bidirectional text clipboard. Talks to the emulator over the
|
||||
* legacy VMware backdoor (port 0x5658; implemented in v86's vmware.js).
|
||||
* Joins the Win32 clipboard-viewer chain so guest copies are pushed
|
||||
* immediately, and polls the backdoor on a timer so host copies show up
|
||||
* within ~250 ms.
|
||||
*
|
||||
* Win9x runs ring-3 code with the I/O bitmap wide open, so a plain IN works
|
||||
* from a user process — no driver needed. On NT this would #GP; we don't run
|
||||
* there.
|
||||
*
|
||||
* Build with Open Watcom v2 (see Makefile). Links USER32/KERNEL32 only,
|
||||
* runs on Win95 RTM.
|
||||
*/
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
|
||||
#define VMW_MAGIC 0x564D5868UL
|
||||
#define VMW_PORT 0x5658
|
||||
#define CMD_GETLEN 6
|
||||
#define CMD_GETDATA 7
|
||||
#define CMD_SETLEN 8
|
||||
#define CMD_SETDATA 9
|
||||
#define CMD_VERSION 10
|
||||
|
||||
#define POLL_MS 250
|
||||
#define MAX_CLIP 0xFFFF
|
||||
|
||||
extern unsigned long bd(unsigned long cmd, unsigned long arg);
|
||||
#pragma aux bd = \
|
||||
"mov eax, 564D5868h" \
|
||||
"mov edx, 5658h" \
|
||||
"in eax, dx" \
|
||||
parm [ecx] [ebx] \
|
||||
value [eax] \
|
||||
modify [edx];
|
||||
|
||||
extern unsigned long bd_ebx(unsigned long cmd, unsigned long arg);
|
||||
#pragma aux bd_ebx = \
|
||||
"mov eax, 564D5868h" \
|
||||
"mov edx, 5658h" \
|
||||
"in eax, dx" \
|
||||
parm [ecx] [ebx] \
|
||||
value [ebx] \
|
||||
modify [eax edx];
|
||||
|
||||
static HWND g_next;
|
||||
static int g_ignore;
|
||||
|
||||
static void push_to_host(HWND hwnd)
|
||||
{
|
||||
HANDLE h;
|
||||
char *p;
|
||||
unsigned long len, i, w;
|
||||
|
||||
if (!IsClipboardFormatAvailable(CF_TEXT)) {
|
||||
bd(CMD_SETLEN, 0);
|
||||
return;
|
||||
}
|
||||
if (!OpenClipboard(hwnd)) return;
|
||||
h = GetClipboardData(CF_TEXT);
|
||||
if (h && (p = (char *)GlobalLock(h)) != 0) {
|
||||
len = lstrlen(p);
|
||||
if (len > MAX_CLIP) len = MAX_CLIP;
|
||||
bd(CMD_SETLEN, len);
|
||||
for (i = 0; i < len; i += 4) {
|
||||
w = (unsigned char)p[i];
|
||||
if (i + 1 < len) w |= (unsigned long)(unsigned char)p[i+1] << 8;
|
||||
if (i + 2 < len) w |= (unsigned long)(unsigned char)p[i+2] << 16;
|
||||
if (i + 3 < len) w |= (unsigned long)(unsigned char)p[i+3] << 24;
|
||||
bd(CMD_SETDATA, w);
|
||||
}
|
||||
GlobalUnlock(h);
|
||||
}
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
static void pull_from_host(HWND hwnd)
|
||||
{
|
||||
long len;
|
||||
unsigned long i, w;
|
||||
HGLOBAL h;
|
||||
char *p;
|
||||
|
||||
len = (long)bd(CMD_GETLEN, 0);
|
||||
if (len < 0) return;
|
||||
if (len > MAX_CLIP) len = MAX_CLIP;
|
||||
|
||||
h = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, (DWORD)len + 1);
|
||||
if (!h) return;
|
||||
p = (char *)GlobalLock(h);
|
||||
for (i = 0; i < (unsigned long)len; i += 4) {
|
||||
w = bd(CMD_GETDATA, 0);
|
||||
p[i] = (char)w;
|
||||
if (i + 1 < (unsigned long)len) p[i+1] = (char)(w >> 8);
|
||||
if (i + 2 < (unsigned long)len) p[i+2] = (char)(w >> 16);
|
||||
if (i + 3 < (unsigned long)len) p[i+3] = (char)(w >> 24);
|
||||
}
|
||||
p[len] = 0;
|
||||
GlobalUnlock(h);
|
||||
|
||||
if (!OpenClipboard(hwnd)) { GlobalFree(h); return; }
|
||||
g_ignore++;
|
||||
EmptyClipboard();
|
||||
SetClipboardData(CF_TEXT, h);
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
switch (msg) {
|
||||
case WM_CREATE:
|
||||
g_next = SetClipboardViewer(hwnd);
|
||||
SetTimer(hwnd, 1, POLL_MS, 0);
|
||||
return 0;
|
||||
|
||||
case WM_DRAWCLIPBOARD:
|
||||
if (g_ignore > 0) g_ignore--;
|
||||
else push_to_host(hwnd);
|
||||
if (g_next) SendMessage(g_next, msg, wp, lp);
|
||||
return 0;
|
||||
|
||||
case WM_CHANGECBCHAIN:
|
||||
if ((HWND)wp == g_next) g_next = (HWND)lp;
|
||||
else if (g_next) SendMessage(g_next, msg, wp, lp);
|
||||
return 0;
|
||||
|
||||
case WM_TIMER:
|
||||
pull_from_host(hwnd);
|
||||
return 0;
|
||||
|
||||
case WM_DESTROY:
|
||||
ChangeClipboardChain(hwnd, g_next);
|
||||
KillTimer(hwnd, 1);
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProc(hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
int PASCAL WinMain(HINSTANCE hi, HINSTANCE hp, LPSTR cmd, int show)
|
||||
{
|
||||
WNDCLASS wc;
|
||||
HWND hwnd;
|
||||
MSG msg;
|
||||
|
||||
(void)hp; (void)cmd; (void)show;
|
||||
|
||||
if (CreateMutex(0, FALSE, "W95Tools") && GetLastError() == ERROR_ALREADY_EXISTS)
|
||||
return 0;
|
||||
|
||||
if (bd_ebx(CMD_VERSION, 0) != VMW_MAGIC) {
|
||||
MessageBox(0, "VMware backdoor not present.", "W95Tools", MB_OK | MB_ICONSTOP);
|
||||
return 1;
|
||||
}
|
||||
|
||||
wc.style = 0;
|
||||
wc.lpfnWndProc = WndProc;
|
||||
wc.cbClsExtra = 0;
|
||||
wc.cbWndExtra = 0;
|
||||
wc.hInstance = hi;
|
||||
wc.hIcon = 0;
|
||||
wc.hCursor = 0;
|
||||
wc.hbrBackground = 0;
|
||||
wc.lpszMenuName = 0;
|
||||
wc.lpszClassName = "W95Tools";
|
||||
RegisterClass(&wc);
|
||||
|
||||
hwnd = CreateWindow("W95Tools", "W95Tools", WS_OVERLAPPED,
|
||||
0, 0, 0, 0, 0, 0, hi, 0);
|
||||
if (!hwnd) return 1;
|
||||
|
||||
while (GetMessage(&msg, 0, 0, 0) > 0) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user