| 1 | #define WIN32_LEAN_AND_MEAN |
| 2 | #define UNICODE |
| 3 | #define _UNICODE |
| 4 | #include <windows.h> |
| 5 | #include "cursor.h" |
| 6 | |
| 7 | typedef HCURSOR (WINAPI *cursor_frame_fn)(HCURSOR, DWORD, DWORD, DWORD *, DWORD *); |
| 8 | static cursor_frame_fn cursor_frame; |
| 9 | static HANDLE output; |
| 10 | static HANDLE heap; |
| 11 | static OVERLAPPED sending; |
| 12 | |
| 13 | static int write_bytes(const void *data, DWORD length) { |
| 14 | const uint8_t *bytes = data; |
| 15 | DWORD written; |
| 16 | while (length) { |
| 17 | ResetEvent(sending.hEvent); |
| 18 | if (!WriteFile(output, bytes, length, &written, &sending)) { |
| 19 | if (GetLastError() != ERROR_IO_PENDING) return 0; |
| 20 | if (WaitForSingleObject(sending.hEvent, 3000) != WAIT_OBJECT_0) { |
| 21 | CancelIo(output); WaitForSingleObject(sending.hEvent, INFINITE); return 0; |
| 22 | } |
| 23 | } |
| 24 | if (!GetOverlappedResult(output, &sending, &written, FALSE)) return 0; |
| 25 | if (!written) return 0; |
| 26 | bytes += written; length -= written; |
| 27 | } |
| 28 | return 1; |
| 29 | } |
| 30 | |
| 31 | static int packet(uint32_t type, uint32_t length, const uint32_t *words, uint32_t count) { |
| 32 | uint8_t header[12], number[4]; |
| 33 | uint32_t i; |
| 34 | header[0] = SG_MAGIC[0]; header[1] = SG_MAGIC[1]; header[2] = SG_MAGIC[2]; header[3] = SG_MAGIC[3]; |
| 35 | sg_word(header + 4, type); sg_word(header + 8, length); |
| 36 | if (!write_bytes(header, sizeof(header))) return 0; |
| 37 | for (i = 0; i < count; ++i) { |
| 38 | sg_word(number, words[i]); |
| 39 | if (!write_bytes(number, sizeof(number))) return 0; |
| 40 | } |
| 41 | return 1; |
| 42 | } |
| 43 | |
| 44 | static int capture_cursor(HCURSOR cursor, uint32_t id) { |
| 45 | ICONINFO icon = {0}; |
| 46 | BITMAP bitmap; |
| 47 | BITMAPINFO info = {0}; |
| 48 | HDC dc = NULL; |
| 49 | HBITMAP dib = NULL, previous = NULL; |
| 50 | uint8_t *bits = NULL, *black = NULL, *frames = NULL; |
| 51 | uint32_t width, height, count = 1, step, words[SG_CURSOR_WORDS] = {0}; |
| 52 | DWORD rate = 6, total = 1; |
| 53 | size_t pixels, frame_bytes; |
| 54 | int success = -1; |
| 55 | if (!GetIconInfo(cursor, &icon)) return -1; |
| 56 | if (!GetObjectW(icon.hbmColor ? icon.hbmColor : icon.hbmMask, sizeof(bitmap), &bitmap)) goto done; |
| 57 | width = (uint32_t)bitmap.bmWidth; |
| 58 | height = (uint32_t)(icon.hbmColor ? bitmap.bmHeight : bitmap.bmHeight / 2); |
| 59 | if (!width || !height || width > SG_MAX_CURSOR_SIZE || height > SG_MAX_CURSOR_SIZE || icon.xHotspot >= width || icon.yHotspot >= height) goto done; |
| 60 | if (cursor_frame && cursor_frame(cursor, 0, 0, &rate, &total) && total > 1 && total <= SG_MAX_CURSOR_FRAMES) count = total; |
| 61 | pixels = (size_t)width * height; |
| 62 | frame_bytes = 4 + pixels * 5; |
| 63 | frames = HeapAlloc(heap, 0, frame_bytes * count); |
| 64 | black = HeapAlloc(heap, 0, pixels * 4); |
| 65 | dc = CreateCompatibleDC(NULL); |
| 66 | info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); info.bmiHeader.biWidth = (LONG)width; |
| 67 | info.bmiHeader.biHeight = -(LONG)height; info.bmiHeader.biPlanes = 1; info.bmiHeader.biBitCount = 32; |
| 68 | dib = CreateDIBSection(dc, &info, DIB_RGB_COLORS, (void **)&bits, NULL, 0); |
| 69 | if (!frames || !black || !dc || !dib) goto done; |
| 70 | previous = SelectObject(dc, dib); |
| 71 | for (step = 0; step < count; ++step) { |
| 72 | uint8_t *frame = frames + frame_bytes * step; |
| 73 | size_t i; |
| 74 | HCURSOR image = cursor; |
| 75 | rate = 6; |
| 76 | if (count > 1) { |
| 77 | image = cursor_frame(cursor, 0, step, &rate, &total); |
| 78 | if (!image || total != count || !rate || rate > 3600) { image = cursor; count = 1; rate = 6; } |
| 79 | } |
| 80 | sg_word(frame, (rate * 1000u + 30u) / 60u); |
| 81 | for (i = 0; i < pixels * 4; ++i) bits[i] = 0; |
| 82 | if (!DrawIconEx(dc, 0, 0, image, (int)width, (int)height, 0, NULL, DI_NORMAL)) goto done; |
| 83 | GdiFlush(); |
| 84 | for (i = 0; i < pixels * 4; ++i) black[i] = bits[i]; |
| 85 | for (i = 0; i < pixels * 4; ++i) bits[i] = 255; |
| 86 | if (!DrawIconEx(dc, 0, 0, image, (int)width, (int)height, 0, NULL, DI_NORMAL)) goto done; |
| 87 | GdiFlush(); |
| 88 | if (!sg_rgba(black, bits, frame + 4, frame + 4 + pixels * 4, pixels)) goto done; |
| 89 | } |
| 90 | words[SG_CURSOR_ID] = id; words[SG_CURSOR_WIDTH] = width; words[SG_CURSOR_HEIGHT] = height; |
| 91 | words[SG_CURSOR_HOT_X] = icon.xHotspot; words[SG_CURSOR_HOT_Y] = icon.yHotspot; words[SG_CURSOR_FRAMES] = count; |
| 92 | success = packet(SG_CURSOR, SG_CURSOR_WORDS * 4 + (uint32_t)(frame_bytes * count), words, SG_CURSOR_WORDS) && write_bytes(frames, (DWORD)(frame_bytes * count)); |
| 93 | done: |
| 94 | if (previous) SelectObject(dc, previous); |
| 95 | if (dib) DeleteObject(dib); |
| 96 | if (dc) DeleteDC(dc); |
| 97 | if (icon.hbmColor) DeleteObject(icon.hbmColor); |
| 98 | if (icon.hbmMask) DeleteObject(icon.hbmMask); |
| 99 | if (black) HeapFree(heap, 0, black); |
| 100 | if (frames) HeapFree(heap, 0, frames); |
| 101 | return success; |
| 102 | } |
| 103 | |
| 104 | static void stream(DWORD session) { |
| 105 | HDC screen = GetDC(NULL), dc = CreateCompatibleDC(screen); |
| 106 | HBITMAP dib = NULL, previous = NULL; |
| 107 | uint8_t *pixels = NULL, *before = NULL; |
| 108 | uint32_t width = 0, height = 0, id = 0; |
| 109 | HCURSOR last_cursor = NULL; |
| 110 | POINT last_point = {-1, -1}; |
| 111 | DWORD last_flags = ~0u, captured = GetTickCount() - 34; |
| 112 | DWORD heartbeat = GetTickCount(), received = 0; |
| 113 | OVERLAPPED receiving = {0}; |
| 114 | uint8_t request[12]; |
| 115 | int pending = 0; |
| 116 | int suspended = 0, initial = 1; |
| 117 | receiving.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL); |
| 118 | if (!screen || !dc || !receiving.hEvent) goto done; |
| 119 | for (;;) { |
| 120 | if (session != WTSGetActiveConsoleSessionId()) goto done; |
| 121 | CURSORINFO cursor = {sizeof(CURSORINFO), 0, NULL, {0, 0}}; |
| 122 | HDESK desktop; |
| 123 | WCHAR name[32]; |
| 124 | DWORD length; |
| 125 | DWORD amount = 0; |
| 126 | if (!pending) { |
| 127 | ResetEvent(receiving.hEvent); |
| 128 | if (!ReadFile(output, request + received, (DWORD)sizeof(request) - received, &amount, &receiving)) { |
| 129 | if (GetLastError() != ERROR_IO_PENDING) goto done; |
| 130 | pending = 1; |
| 131 | } else if (!GetOverlappedResult(output, &receiving, &amount, FALSE)) goto done; |
| 132 | } else if (GetOverlappedResult(output, &receiving, &amount, FALSE)) pending = 0; |
| 133 | else if (GetLastError() != ERROR_IO_INCOMPLETE) goto done; |
| 134 | if (!pending) { |
| 135 | if (!amount) goto done; |
| 136 | received += amount; |
| 137 | if (received == sizeof(request)) { |
| 138 | uint8_t expected[12] = {SG_MAGIC[0], SG_MAGIC[1], SG_MAGIC[2], SG_MAGIC[3], 0,0,0,0, 0,0,0,0}; |
| 139 | uint32_t i; |
| 140 | sg_word(expected + 4, SG_REFRESH); |
| 141 | for (i = 0; i < sizeof(request); ++i) if (request[i] != expected[i]) goto done; |
| 142 | received = 0; initial = 1; last_cursor = NULL; last_flags = ~0u; |
| 143 | } |
| 144 | } |
| 145 | if ((DWORD)(GetTickCount() - heartbeat) >= 1000) { |
| 146 | if (!packet(SG_HEARTBEAT, 0, NULL, 0)) goto done; |
| 147 | heartbeat = GetTickCount(); |
| 148 | } |
| 149 | desktop = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS); |
| 150 | int active = desktop && GetUserObjectInformationW(desktop, UOI_NAME, name, sizeof(name), &length) && name[0] == L'D' && name[1] == L'e' && name[2] == L'f' && name[3] == L'a' && name[4] == L'u' && name[5] == L'l' && name[6] == L't' && !name[7]; |
| 151 | if (desktop) CloseDesktop(desktop); |
| 152 | if (!active || !GetCursorInfo(&cursor)) { |
| 153 | if (!suspended && !packet(SG_SUSPEND, 0, NULL, 0)) goto done; |
| 154 | suspended = 1; initial = 1; last_cursor = NULL; last_flags = ~0u; |
| 155 | Sleep(100); continue; |
| 156 | } |
| 157 | int changed = cursor.hCursor && cursor.hCursor != last_cursor; |
| 158 | if (changed) ++id; |
| 159 | if (changed || cursor.flags != last_flags || cursor.ptScreenPos.x != last_point.x || cursor.ptScreenPos.y != last_point.y) { |
| 160 | uint32_t words[SG_POINTER_WORDS] = {0}; |
| 161 | words[SG_POINTER_ID] = id; words[SG_POINTER_X] = (uint32_t)cursor.ptScreenPos.x; |
| 162 | words[SG_POINTER_Y] = (uint32_t)cursor.ptScreenPos.y; words[SG_POINTER_VISIBLE] = (cursor.flags & CURSOR_SHOWING) ? 1 : 0; |
| 163 | if (!packet(SG_POINTER, sizeof(words), words, SG_POINTER_WORDS)) goto done; |
| 164 | last_flags = cursor.flags; last_point = cursor.ptScreenPos; |
| 165 | } |
| 166 | if (changed) { |
| 167 | int result = capture_cursor(cursor.hCursor, id); |
| 168 | if (!result) goto done; |
| 169 | if (result < 0) { |
| 170 | uint32_t words[SG_CURSOR_WORDS] = {0}; |
| 171 | words[SG_CURSOR_ID] = id; |
| 172 | if (!packet(SG_CURSOR, sizeof(words), words, SG_CURSOR_WORDS)) goto done; |
| 173 | } |
| 174 | last_cursor = cursor.hCursor; |
| 175 | } |
| 176 | if ((DWORD)(GetTickCount() - captured) >= 33) { |
| 177 | uint32_t w = (uint32_t)GetSystemMetrics(SM_CXSCREEN), h = (uint32_t)GetSystemMetrics(SM_CYSCREEN), y; |
| 178 | struct sg_rect rect; |
| 179 | uint32_t words[SG_SCREEN_WORDS] = {0}; |
| 180 | if (!w || !h || w > SG_MAX_DIMENSION || h > SG_MAX_DIMENSION || w * h > SG_MAX_PIXELS) goto done; |
| 181 | if (w != width || h != height) { |
| 182 | BITMAPINFO info = {0}; |
| 183 | if (previous) { SelectObject(dc, previous); previous = NULL; } |
| 184 | if (dib) DeleteObject(dib); |
| 185 | if (before) HeapFree(heap, 0, before); |
| 186 | before = HeapAlloc(heap, 0, (size_t)w * h * 4); |
| 187 | info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); info.bmiHeader.biWidth = (LONG)w; |
| 188 | info.bmiHeader.biHeight = -(LONG)h; info.bmiHeader.biPlanes = 1; info.bmiHeader.biBitCount = 32; |
| 189 | dib = CreateDIBSection(dc, &info, DIB_RGB_COLORS, (void **)&pixels, NULL, 0); |
| 190 | if (!before || !dib) goto done; |
| 191 | previous = SelectObject(dc, dib); width = w; height = h; initial = 1; |
| 192 | } |
| 193 | if (!BitBlt(dc, 0, 0, (int)width, (int)height, screen, 0, 0, SRCCOPY | CAPTUREBLT)) { |
| 194 | if (!suspended && !packet(SG_SUSPEND, 0, NULL, 0)) goto done; |
| 195 | suspended = 1; initial = 1; Sleep(100); continue; |
| 196 | } |
| 197 | GdiFlush(); |
| 198 | if (initial) { rect.x = 0; rect.y = 0; rect.width = width; rect.height = height; } |
| 199 | else if (!sg_difference(before, pixels, width, height, &rect)) { captured = GetTickCount(); Sleep(8); continue; } |
| 200 | words[SG_SCREEN_WIDTH] = width; words[SG_SCREEN_HEIGHT] = height; |
| 201 | words[SG_SCREEN_X] = rect.x; words[SG_SCREEN_Y] = rect.y; |
| 202 | words[SG_SCREEN_RECT_WIDTH] = rect.width; words[SG_SCREEN_RECT_HEIGHT] = rect.height; |
| 203 | if (!packet(SG_SCREEN, sizeof(words) + rect.width * rect.height * 4, words, SG_SCREEN_WORDS)) goto done; |
| 204 | for (y = rect.y; y < rect.y + rect.height; ++y) { |
| 205 | size_t offset = ((size_t)y * width + rect.x) * 4, i; |
| 206 | if (!write_bytes(pixels + offset, rect.width * 4)) goto done; |
| 207 | for (i = 0; i < rect.width * 4; ++i) before[offset + i] = pixels[offset + i]; |
| 208 | } |
| 209 | initial = 0; suspended = 0; captured = GetTickCount(); |
| 210 | } |
| 211 | Sleep(8); |
| 212 | } |
| 213 | done: |
| 214 | CancelIo(output); |
| 215 | if (pending) WaitForSingleObject(receiving.hEvent, INFINITE); |
| 216 | if (receiving.hEvent) CloseHandle(receiving.hEvent); |
| 217 | if (previous) SelectObject(dc, previous); |
| 218 | if (dib) DeleteObject(dib); |
| 219 | if (before) HeapFree(heap, 0, before); |
| 220 | if (dc) DeleteDC(dc); |
| 221 | if (screen) ReleaseDC(NULL, screen); |
| 222 | } |
| 223 | |
| 224 | void mainCRTStartup(void) { |
| 225 | FARPROC symbol; |
| 226 | DWORD session; |
| 227 | if (!ProcessIdToSessionId(GetCurrentProcessId(), &session)) ExitProcess(1); |
| 228 | heap = GetProcessHeap(); |
| 229 | sending.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL); |
| 230 | if (!sending.hEvent) ExitProcess(1); |
| 231 | SetProcessDPIAware(); |
| 232 | symbol = GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetCursorFrameInfo"); |
| 233 | /* This optional user32 export supplies ANI step timing; static capture uses only documented APIs. */ |
| 234 | cursor_frame = (cursor_frame_fn)symbol; |
| 235 | for (;;) { |
| 236 | if (session != WTSGetActiveConsoleSessionId()) { Sleep(1000); continue; } |
| 237 | output = CreateFileW(L"\\\\.\\Global\\" SG_CHANNEL, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); |
| 238 | if (output != INVALID_HANDLE_VALUE) { stream(session); CloseHandle(output); } |
| 239 | Sleep(1000); |
| 240 | } |
| 241 | } |