/* Looking Glass - KVM FrameRelay (KVMFR) Client Copyright (C) 2017-2019 Geoffrey McRae https://looking-glass.hostfission.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "debug.h" #include "utils.h" #include "KVMFR.h" #include "spice/spice.h" #include "kb.h" #include "lg-renderers.h" #include "lg-clipboards.h" struct AppState { bool running; bool keyDown[SDL_NUM_SCANCODES]; bool haveSrcSize; int windowW, windowH; SDL_Point srcSize; LG_RendererRect dstRect; SDL_Point cursor; bool cursorVisible; bool haveCursorPos; float scaleX, scaleY; float accX, accY; const LG_Renderer * lgr; void * lgrData; bool lgrResize; const LG_Clipboard * lgc; SpiceDataType cbType; LG_ClipboardReplyFn cbReplyFn; void * cbReplyData; SDL_Window * window; int shmFD; struct KVMFRHeader * shm; unsigned int shmSize; uint64_t frameTime; uint64_t lastFrameTime; uint64_t renderTime; uint64_t frameCount; uint64_t renderCount; }; typedef struct RenderOpts { unsigned int size; unsigned int argc; LG_RendererOptValue * argv; } RendererOpts; struct AppParams { const char * configFile; bool autoResize; bool allowResize; bool keepAspect; bool borderless; bool fullscreen; bool center; int x, y; unsigned int w, h; char * shmFile; unsigned int shmSize; unsigned int fpsLimit; bool showFPS; bool useSpice; char * spiceHost; unsigned int spicePort; bool scaleMouseInput; bool hideMouse; bool ignoreQuit; bool allowScreensaver; bool grabKeyboard; SDL_Scancode captureKey; bool disableAlerts; bool forceRenderer; unsigned int forceRendererIndex; RendererOpts rendererOpts[LG_RENDERER_COUNT]; }; struct AppState state; struct AppParams params = { .configFile = "/etc/looking-glass.conf", .autoResize = false, .allowResize = true, .keepAspect = true, .borderless = false, .fullscreen = false, .center = true, .x = 0, .y = 0, .w = 1024, .h = 768, .shmFile = "/dev/shm/looking-glass", .shmSize = 0, .fpsLimit = 200, .showFPS = false, .useSpice = true, .spiceHost = "127.0.0.1", .spicePort = 5900, .scaleMouseInput = true, .hideMouse = true, .ignoreQuit = false, .allowScreensaver = true, .grabKeyboard = true, .captureKey = SDL_SCANCODE_SCROLLLOCK, .disableAlerts = false, .forceRenderer = false }; static void updatePositionInfo() { if (state.haveSrcSize) { if (params.keepAspect) { const float srcAspect = (float)state.srcSize.y / (float)state.srcSize.x; const float wndAspect = (float)state.windowH / (float)state.windowW; if (wndAspect < srcAspect) { state.dstRect.w = (float)state.windowH / srcAspect; state.dstRect.h = state.windowH; state.dstRect.x = (state.windowW >> 1) - (state.dstRect.w >> 1); state.dstRect.y = 0; } else { state.dstRect.w = state.windowW; state.dstRect.h = (float)state.windowW * srcAspect; state.dstRect.x = 0; state.dstRect.y = (state.windowH >> 1) - (state.dstRect.h >> 1); } } else { state.dstRect.x = 0; state.dstRect.y = 0; state.dstRect.w = state.windowW; state.dstRect.h = state.windowH; } state.dstRect.valid = true; state.scaleX = (float)state.srcSize.y / (float)state.dstRect.h; state.scaleY = (float)state.srcSize.x / (float)state.dstRect.w; } state.lgrResize = true; } int renderThread(void * unused) { if (!state.lgr->render_startup(state.lgrData, state.window)) return 1; struct timespec time; clock_gettime(CLOCK_MONOTONIC, &time); while(state.running) { if (state.lgrResize) { if (state.lgr) state.lgr->on_resize(state.lgrData, state.windowW, state.windowH, state.dstRect); state.lgrResize = false; } if (!state.lgr->render(state.lgrData, state.window)) break; if (params.showFPS) { const uint64_t t = nanotime(); state.renderTime += t - state.lastFrameTime; state.lastFrameTime = t; ++state.renderCount; if (state.renderTime > 1e9) { const float avgUPS = 1000.0f / (((float)state.renderTime / state.frameCount ) / 1e6f); const float avgFPS = 1000.0f / (((float)state.renderTime / state.renderCount) / 1e6f); state.lgr->update_fps(state.lgrData, avgUPS, avgFPS); state.renderTime = 0; state.frameCount = 0; state.renderCount = 0; } } uint64_t nsec = time.tv_nsec + state.frameTime; if (nsec > 1e9) { time.tv_nsec = nsec - 1e9; ++time.tv_sec; } else time.tv_nsec = nsec; clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &time, NULL); } return 0; } int cursorThread(void * unused) { KVMFRCursor header; LG_RendererCursor cursorType = LG_CURSOR_COLOR; uint32_t version = 0; memset(&header, 0, sizeof(KVMFRCursor)); while(state.running) { // poll until we have cursor data if(!(state.shm->cursor.flags & KVMFR_CURSOR_FLAG_UPDATE) && !(state.shm->cursor.flags & KVMFR_CURSOR_FLAG_POS)) { if (!state.running) return 0; usleep(1); continue; } // if the cursor was moved bool moved = false; if (state.shm->cursor.flags & KVMFR_CURSOR_FLAG_POS) { state.cursor.x = state.shm->cursor.x; state.cursor.y = state.shm->cursor.y; state.haveCursorPos = true; moved = true; } // if this was only a move event if (!(state.shm->cursor.flags & KVMFR_CURSOR_FLAG_UPDATE)) { // turn off the pos flag, trigger the event and continue __sync_and_and_fetch(&state.shm->cursor.flags, ~KVMFR_CURSOR_FLAG_POS); state.lgr->on_mouse_event ( state.lgrData, state.cursorVisible, state.cursor.x, state.cursor.y ); continue; } // we must take a copy of the header to prevent the contained arguments // from being abused to overflow buffers. memcpy(&header, &state.shm->cursor, sizeof(struct KVMFRCursor)); if (header.flags & KVMFR_CURSOR_FLAG_SHAPE && header.version != version) { version = header.version; bool bad = false; switch(header.type) { case CURSOR_TYPE_COLOR : cursorType = LG_CURSOR_COLOR ; break; case CURSOR_TYPE_MONOCHROME : cursorType = LG_CURSOR_MONOCHROME ; break; case CURSOR_TYPE_MASKED_COLOR: cursorType = LG_CURSOR_MASKED_COLOR; break; default: DEBUG_ERROR("Invalid cursor type"); bad = true; break; } if (bad) break; // check the data position is sane const uint64_t dataSize = header.height * header.pitch; if (header.dataPos + dataSize > state.shmSize) { DEBUG_ERROR("The guest sent an invalid mouse dataPos"); break; } const uint8_t * data = (const uint8_t *)state.shm + header.dataPos; if (!state.lgr->on_mouse_shape( state.lgrData, cursorType, header.width, header.height, header.pitch, data) ) { DEBUG_ERROR("Failed to update mouse shape"); break; } } // now we have taken the mouse data, we can flag to the host we are ready state.shm->cursor.flags = 0; bool showCursor = header.flags & KVMFR_CURSOR_FLAG_VISIBLE; if (showCursor != state.cursorVisible || moved) { state.cursorVisible = showCursor; state.lgr->on_mouse_event ( state.lgrData, state.cursorVisible, state.cursor.x, state.cursor.y ); } } return 0; } int frameThread(void * unused) { bool error = false; KVMFRFrame header; memset(&header, 0, sizeof(struct KVMFRFrame)); SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH); while(state.running) { // poll until we have a new frame while(!(state.shm->frame.flags & KVMFR_FRAME_FLAG_UPDATE)) { if (!state.running) break; usleep(1); continue; } // we must take a copy of the header to prevent the contained // arguments from being abused to overflow buffers. memcpy(&header, &state.shm->frame, sizeof(struct KVMFRFrame)); // tell the host to continue as the host buffers up to one frame // we can be sure the data for this frame wont be touched __sync_and_and_fetch(&state.shm->frame.flags, ~KVMFR_FRAME_FLAG_UPDATE); // sainty check of the frame format if ( header.type >= FRAME_TYPE_MAX || header.width == 0 || header.height == 0 || header.pitch == 0 || header.dataPos == 0 || header.dataPos > state.shmSize || header.pitch < header.width ){ DEBUG_WARN("Bad header"); usleep(1000); continue; } // setup the renderer format with the frame format details LG_RendererFormat lgrFormat; lgrFormat.type = header.type; lgrFormat.width = header.width; lgrFormat.height = header.height; lgrFormat.stride = header.stride; lgrFormat.pitch = header.pitch; size_t dataSize; switch(header.type) { case FRAME_TYPE_RGBA: case FRAME_TYPE_BGRA: case FRAME_TYPE_RGBA10: dataSize = lgrFormat.height * lgrFormat.pitch; lgrFormat.bpp = 32; break; case FRAME_TYPE_YUV420: dataSize = lgrFormat.height * lgrFormat.width; dataSize += (dataSize / 4) * 2; lgrFormat.bpp = 12; break; default: DEBUG_ERROR("Unsupported frameType"); error = true; break; } if (error) break; // check the header's dataPos is sane if (header.dataPos + dataSize > state.shmSize) { DEBUG_ERROR("The guest sent an invalid dataPos"); break; } if (header.width != state.srcSize.x || header.height != state.srcSize.y) { state.srcSize.x = header.width; state.srcSize.y = header.height; state.haveSrcSize = true; if (params.autoResize) SDL_SetWindowSize(state.window, header.width, header.height); updatePositionInfo(); } const uint8_t * data = (const uint8_t *)state.shm + header.dataPos; if (!state.lgr->on_frame_event(state.lgrData, lgrFormat, data)) { DEBUG_ERROR("renderer on frame event returned failure"); break; } ++state.frameCount; } state.running = false; return 0; } int spiceThread(void * arg) { while(state.running) if (!spice_process()) { if (state.running) { state.running = false; DEBUG_ERROR("failed to process spice messages"); } break; } state.running = false; return 0; } static inline const uint32_t mapScancode(SDL_Scancode scancode) { uint32_t ps2; if (scancode > (sizeof(usb_to_ps2) / sizeof(uint32_t)) || (ps2 = usb_to_ps2[scancode]) == 0) { DEBUG_WARN("Unable to map USB scan code: %x\n", scancode); return 0; } return ps2; } static LG_ClipboardData spice_type_to_clipboard_type(const SpiceDataType type) { switch(type) { case SPICE_DATA_TEXT: return LG_CLIPBOARD_DATA_TEXT; break; case SPICE_DATA_PNG : return LG_CLIPBOARD_DATA_PNG ; break; case SPICE_DATA_BMP : return LG_CLIPBOARD_DATA_BMP ; break; case SPICE_DATA_TIFF: return LG_CLIPBOARD_DATA_TIFF; break; case SPICE_DATA_JPEG: return LG_CLIPBOARD_DATA_JPEG; break; default: DEBUG_ERROR("invalid spice data type"); return LG_CLIPBOARD_DATA_NONE; } } static SpiceDataType clipboard_type_to_spice_type(const LG_ClipboardData type) { switch(type) { case LG_CLIPBOARD_DATA_TEXT: return SPICE_DATA_TEXT; break; case LG_CLIPBOARD_DATA_PNG : return SPICE_DATA_PNG ; break; case LG_CLIPBOARD_DATA_BMP : return SPICE_DATA_BMP ; break; case LG_CLIPBOARD_DATA_TIFF: return SPICE_DATA_TIFF; break; case LG_CLIPBOARD_DATA_JPEG: return SPICE_DATA_JPEG; break; default: DEBUG_ERROR("invalid clipboard data type"); return SPICE_DATA_NONE; } } void clipboardRelease() { spice_clipboard_release(); } void clipboardNotify(const LG_ClipboardData type) { if (type == LG_CLIPBOARD_DATA_NONE) { spice_clipboard_release(); return; } spice_clipboard_grab(clipboard_type_to_spice_type(type)); } void clipboardData(const LG_ClipboardData type, uint8_t * data, size_t size) { uint8_t * buffer = data; // unix2dos if (type == LG_CLIPBOARD_DATA_TEXT) { // TODO: make this more memory efficent buffer = malloc(size * 2); uint8_t * p = buffer; for(uint32_t i = 0; i < size; ++i) { uint8_t c = data[i]; if (c == '\n') *p++ = '\r'; *p++ = c; } } spice_clipboard_data(clipboard_type_to_spice_type(type), buffer, (uint32_t)size); if (buffer != data) free(buffer); } void clipboardRequest(const LG_ClipboardReplyFn replyFn, void * opaque) { state.cbReplyData = opaque; state.cbReplyFn = replyFn; spice_clipboard_request(state.cbType); } void spiceClipboardNotice(const SpiceDataType type) { if (!state.lgc || !state.lgc->notice) return; state.cbType = type; state.lgc->notice(clipboardRequest, spice_type_to_clipboard_type(type)); } void spiceClipboardData(const SpiceDataType type, uint8_t * buffer, uint32_t size) { if (type == SPICE_DATA_TEXT) { // dos2unix uint8_t * p = buffer; for(uint32_t i = 0; i < size; ++i) { uint8_t c = buffer[i]; if (c != '\r') *p++ = c; } } state.cbReplyFn(state.cbReplyData, type, buffer, size); } void spiceClipboardRelease() { if (state.lgc && state.lgc->release) state.lgc->release(); } void spiceClipboardRequest(const SpiceDataType type) { if (state.lgc && state.lgc->request) state.lgc->request(spice_type_to_clipboard_type(type)); } int eventFilter(void * userdata, SDL_Event * event) { static bool serverMode = false; static bool realignGuest = true; switch(event->type) { case SDL_QUIT: { if (!params.ignoreQuit) state.running = false; return 0; } case SDL_WINDOWEVENT: { switch(event->window.event) { case SDL_WINDOWEVENT_ENTER: realignGuest = true; break; case SDL_WINDOWEVENT_SIZE_CHANGED: SDL_GetWindowSize(state.window, &state.windowW, &state.windowH); updatePositionInfo(); realignGuest = true; break; } return 0; } } if (!params.useSpice) return 0; switch(event->type) { case SDL_SYSWMEVENT: { if (state.lgc && state.lgc->wmevent) state.lgc->wmevent(event->syswm.msg); return 0; } case SDL_MOUSEMOTION: { if ( !serverMode && ( event->motion.x < state.dstRect.x || event->motion.x > state.dstRect.x + state.dstRect.w || event->motion.y < state.dstRect.y || event->motion.y > state.dstRect.y + state.dstRect.h ) ) { realignGuest = true; break; } int x = 0; int y = 0; if (realignGuest && state.haveCursorPos) { x = event->motion.x - state.dstRect.x; y = event->motion.y - state.dstRect.y; if (params.scaleMouseInput && !serverMode) { x = (float)x * state.scaleX; y = (float)y * state.scaleY; } x -= state.cursor.x; y -= state.cursor.y; realignGuest = false; state.accX = 0; state.accY = 0; if (!spice_mouse_motion(x, y)) DEBUG_ERROR("SDL_MOUSEMOTION: failed to send message"); break; } x = event->motion.xrel; y = event->motion.yrel; if (x != 0 || y != 0) { if (params.scaleMouseInput && !serverMode) { state.accX += (float)x * state.scaleX; state.accY += (float)y * state.scaleY; x = floor(state.accX); y = floor(state.accY); state.accX -= x; state.accY -= y; } if (!spice_mouse_motion(x, y)) { DEBUG_ERROR("SDL_MOUSEMOTION: failed to send message"); break; } } break; } case SDL_KEYDOWN: { SDL_Scancode sc = event->key.keysym.scancode; if (sc == params.captureKey) { if (event->key.repeat) break; serverMode = !serverMode; spice_mouse_mode(serverMode); SDL_SetRelativeMouseMode(serverMode); SDL_SetWindowGrab(state.window, serverMode); DEBUG_INFO("Server Mode: %s", serverMode ? "on" : "off"); if (state.lgr && !params.disableAlerts) state.lgr->on_alert( state.lgrData, serverMode ? LG_ALERT_SUCCESS : LG_ALERT_WARNING, serverMode ? "Capture Enabled" : "Capture Disabled", NULL ); if (!serverMode) realignGuest = true; break; } uint32_t scancode = mapScancode(sc); if (scancode == 0) break; if (!state.keyDown[sc]) { if (spice_key_down(scancode)) state.keyDown[sc] = true; else { DEBUG_ERROR("SDL_KEYDOWN: failed to send message"); break; } } break; } case SDL_KEYUP: { SDL_Scancode sc = event->key.keysym.scancode; if (sc == params.captureKey) break; // avoid sending key up events when we didn't send a down if (!state.keyDown[sc]) break; uint32_t scancode = mapScancode(sc); if (scancode == 0) break; if (spice_key_up(scancode)) state.keyDown[sc] = false; else { DEBUG_ERROR("SDL_KEYUP: failed to send message"); break; } break; } case SDL_MOUSEWHEEL: if ( !spice_mouse_press (event->wheel.y == 1 ? 4 : 5) || !spice_mouse_release(event->wheel.y == 1 ? 4 : 5) ) { DEBUG_ERROR("SDL_MOUSEWHEEL: failed to send messages"); break; } break; case SDL_MOUSEBUTTONDOWN: // The SPICE protocol doesn't support more than a standard PS/2 3 button mouse if (event->button.button > 3) break; if ( !spice_mouse_position(event->button.x, event->button.y) || !spice_mouse_press(event->button.button) ) { DEBUG_ERROR("SDL_MOUSEBUTTONDOWN: failed to send message"); break; } break; case SDL_MOUSEBUTTONUP: // The SPICE protocol doesn't support more than a standard PS/2 3 button mouse if (event->button.button > 3) break; if ( !spice_mouse_position(event->button.x, event->button.y) || !spice_mouse_release(event->button.button) ) { DEBUG_ERROR("SDL_MOUSEBUTTONUP: failed to send message"); break; } break; } // consume all events return 0; } void intHandler(int signal) { switch(signal) { case SIGINT: state.running = false; break; } } static void * map_memory() { struct stat st; if (stat(params.shmFile, &st) < 0) { DEBUG_ERROR("Failed to stat the shared memory file: %s", params.shmFile); return NULL; } state.shmSize = params.shmSize ? params.shmSize : st.st_size; state.shmFD = open(params.shmFile, O_RDWR, (mode_t)0600); if (state.shmFD < 0) { DEBUG_ERROR("Failed to open the shared memory file: %s", params.shmFile); return NULL; } void * map = mmap(0, state.shmSize, PROT_READ | PROT_WRITE, MAP_SHARED, state.shmFD, 0); if (map == MAP_FAILED) { DEBUG_ERROR("Failed to map the shared memory file: %s", params.shmFile); close(state.shmFD); state.shmFD = 0; return NULL; } return map; } static bool try_renderer(const int index, const LG_RendererParams lgrParams, Uint32 * sdlFlags) { const LG_Renderer *r = LG_Renderers[index]; RendererOpts *opts = ¶ms.rendererOpts[index]; if (!IS_LG_RENDERER_VALID(r)) { DEBUG_ERROR("FIXME: Renderer %d is invalid, skipping", index); return false; } // create the renderer state.lgrData = NULL; if (!r->create(&state.lgrData, lgrParams)) return false; // set it's options for(unsigned int i = 0; i < opts->argc; ++i) opts->argv[i].opt->handler(state.lgrData, opts->argv[i].value); // initialize the renderer if (!r->initialize(state.lgrData, sdlFlags)) { r->deinitialize(state.lgrData); return false; } DEBUG_INFO("Using Renderer: %s", r->get_name()); return true; } int run() { DEBUG_INFO("Looking Glass (" BUILD_VERSION ")"); DEBUG_INFO("Locking Method: " LG_LOCK_MODE); memset(&state, 0, sizeof(state)); state.running = true; state.scaleX = 1.0f; state.scaleY = 1.0f; state.frameTime = 1e9 / params.fpsLimit; char* XDG_SESSION_TYPE = getenv("XDG_SESSION_TYPE"); if (XDG_SESSION_TYPE == NULL) XDG_SESSION_TYPE = "unspecified"; if (strcmp(XDG_SESSION_TYPE, "wayland") == 0) { DEBUG_INFO("Wayland detected"); int err = setenv("SDL_VIDEODRIVER", "wayland", 1); if (err < 0) { DEBUG_ERROR("Unable to set the env variable SDL_VIDEODRIVER: %d", err); return -1; } DEBUG_INFO("SDL_VIDEODRIVER has been set to wayland"); } // warn about using FPS display until we can fix the font rendering to prevent lag spikes if (params.showFPS) { DEBUG_WARN("================================================================================"); DEBUG_WARN("WARNING: The FPS display causes microstutters, this is a known issue" ); DEBUG_WARN("================================================================================"); } if (SDL_Init(SDL_INIT_VIDEO) < 0) { DEBUG_ERROR("SDL_Init Failed"); return -1; } // override SDL's SIGINIT handler so that we can tell the difference between // SIGINT and the user sending a close event, such as ALT+F4 signal(SIGINT, intHandler); LG_RendererParams lgrParams; lgrParams.showFPS = params.showFPS; Uint32 sdlFlags; if (params.forceRenderer) { DEBUG_INFO("Trying forced renderer"); sdlFlags = 0; if (!try_renderer(params.forceRendererIndex, lgrParams, &sdlFlags)) { DEBUG_ERROR("Forced renderer failed to iniailize"); return -1; } state.lgr = LG_Renderers[params.forceRendererIndex]; } else { // probe for a a suitable renderer for(unsigned int i = 0; i < LG_RENDERER_COUNT; ++i) { sdlFlags = 0; if (try_renderer(i, lgrParams, &sdlFlags)) { state.lgr = LG_Renderers[i]; break; } } } if (!state.lgr) { DEBUG_INFO("Unable to find a suitable renderer"); return -1; } state.window = SDL_CreateWindow( "Looking Glass (Client)", params.center ? SDL_WINDOWPOS_CENTERED : params.x, params.center ? SDL_WINDOWPOS_CENTERED : params.y, params.w, params.h, ( SDL_WINDOW_SHOWN | (params.fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0) | (params.allowResize ? SDL_WINDOW_RESIZABLE : 0) | (params.borderless ? SDL_WINDOW_BORDERLESS : 0) | sdlFlags ) ); if (state.window == NULL) { DEBUG_ERROR("Could not create an SDL window: %s\n", SDL_GetError()); return 1; } if (params.fullscreen) SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0"); if (params.allowScreensaver) SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); if (!params.center) SDL_SetWindowPosition(state.window, params.x, params.y); // ensure the initial window size is stored in the state SDL_GetWindowSize(state.window, &state.windowW, &state.windowH); // ensure renderer viewport is aware of the current window size updatePositionInfo(); // set the compositor hint to bypass for low latency SDL_SysWMinfo wminfo; SDL_VERSION(&wminfo.version); if (SDL_GetWindowWMInfo(state.window, &wminfo)) { if (wminfo.subsystem == SDL_SYSWM_X11) { Atom NETWM_BYPASS_COMPOSITOR = XInternAtom( wminfo.info.x11.display, "NETWM_BYPASS_COMPOSITOR", False); unsigned long value = 1; XChangeProperty( wminfo.info.x11.display, wminfo.info.x11.window, NETWM_BYPASS_COMPOSITOR, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&value, 1 ); state.lgc = &LGC_X11; } } else { DEBUG_ERROR("Could not get SDL window information %s", SDL_GetError()); return -1; } if (!state.window) { DEBUG_ERROR("failed to create window"); return -1; } if (state.lgc) { DEBUG_INFO("Using Clipboard: %s", state.lgc->getName()); if (!state.lgc->init(&wminfo, clipboardRelease, clipboardNotify, clipboardData)) { DEBUG_WARN("Failed to initialize the clipboard interface, continuing anyway"); state.lgc = NULL; } } SDL_Cursor *cursor = NULL; if (params.hideMouse) { // work around SDL_ShowCursor being non functional int32_t cursorData[2] = {0, 0}; cursor = SDL_CreateCursor((uint8_t*)cursorData, (uint8_t*)cursorData, 8, 8, 4, 4); SDL_SetCursor(cursor); SDL_ShowCursor(SDL_DISABLE); } SDL_Thread *t_spice = NULL; SDL_Thread *t_main = NULL; SDL_Thread *t_frame = NULL; SDL_Thread *t_render = NULL; while(1) { state.shm = (struct KVMFRHeader *)map_memory(); if (!state.shm) { DEBUG_ERROR("Failed to map memory"); break; } // start the renderThread so we don't just display junk if (!(t_render = SDL_CreateThread(renderThread, "renderThread", NULL))) { DEBUG_ERROR("render create thread failed"); break; } if (params.useSpice) { spice_set_clipboard_cb( spiceClipboardNotice, spiceClipboardData, spiceClipboardRelease, spiceClipboardRequest); if (!spice_connect(params.spiceHost, params.spicePort, "")) { DEBUG_ERROR("Failed to connect to spice server"); return 0; } while(state.running && !spice_ready()) if (!spice_process()) { state.running = false; DEBUG_ERROR("Failed to process spice messages"); break; } if (!(t_spice = SDL_CreateThread(spiceThread, "spiceThread", NULL))) { DEBUG_ERROR("spice create thread failed"); break; } } // ensure mouse acceleration is identical in server mode SDL_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "1", SDL_HINT_OVERRIDE); SDL_SetEventFilter(eventFilter, NULL); // flag the host that we are starting up this is important so that // the host wakes up if it is waiting on an interrupt, the host will // also send us the current mouse shape since we won't know it yet DEBUG_INFO("Waiting for host to signal it's ready..."); __sync_or_and_fetch(&state.shm->flags, KVMFR_HEADER_FLAG_RESTART); while(state.running && (state.shm->flags & KVMFR_HEADER_FLAG_RESTART)) SDL_WaitEventTimeout(NULL, 1000); if (!state.running) break; DEBUG_INFO("Host ready, starting session"); // check the header's magic and version are valid if (memcmp(state.shm->magic, KVMFR_HEADER_MAGIC, sizeof(KVMFR_HEADER_MAGIC)) != 0) { DEBUG_ERROR("Invalid header magic, is the host running?"); break; } if (state.shm->version != KVMFR_HEADER_VERSION) { DEBUG_ERROR("KVMFR version missmatch, expected %u but got %u", KVMFR_HEADER_VERSION, state.shm->version); DEBUG_ERROR("This is not a bug, ensure you have the right version of looking-glass-host.exe on the guest"); break; } if (!(t_main = SDL_CreateThread(cursorThread, "cursorThread", NULL))) { DEBUG_ERROR("cursor create thread failed"); break; } if (!(t_frame = SDL_CreateThread(frameThread, "frameThread", NULL))) { DEBUG_ERROR("frame create thread failed"); break; } bool *closeAlert = NULL; while(state.running) { SDL_WaitEventTimeout(NULL, 1000); if (closeAlert == NULL) { if (state.shm->flags & KVMFR_HEADER_FLAG_PAUSED) { if (state.lgr && !params.disableAlerts) state.lgr->on_alert( state.lgrData, LG_ALERT_WARNING, "Stream Paused", &closeAlert ); } } else { if (!(state.shm->flags & KVMFR_HEADER_FLAG_PAUSED)) { *closeAlert = true; closeAlert = NULL; } } } break; } state.running = false; if (t_render) SDL_WaitThread(t_render, NULL); if (t_frame) SDL_WaitThread(t_frame, NULL); if (t_main) SDL_WaitThread(t_main, NULL); // if spice is still connected send key up events for any pressed keys if (params.useSpice && spice_ready()) { for(int i = 0; i < SDL_NUM_SCANCODES; ++i) if (state.keyDown[i]) { uint32_t scancode = mapScancode(i); if (scancode == 0) continue; state.keyDown[i] = false; spice_key_up(scancode); } if (t_spice) SDL_WaitThread(t_spice, NULL); spice_disconnect(); } if (state.lgr) state.lgr->deinitialize(state.lgrData); if (state.lgc) state.lgc->free(); if (state.window) SDL_DestroyWindow(state.window); if (cursor) SDL_FreeCursor(cursor); if (state.shm) { munmap(state.shm, state.shmSize); close(state.shmFD); } SDL_Quit(); return 0; } void doHelp(char * app) { char x[8], y[8]; snprintf(x, sizeof(x), "%d", params.x); snprintf(y, sizeof(y), "%d", params.y); fprintf(stderr, "Looking Glass Client\n" "Usage: %s [OPTION]...\n" "Example: %s -h\n" "\n" " -h Print out this help\n" "\n" " -C PATH Specify an additional configuration file to load\n" " -f PATH Specify the path to the shared memory file [current: %s]\n" " -L SIZE Specify the size in MB of the shared memory file (0 = detect) [current: %d]\n" "\n" " -s Disable spice client\n" " -c HOST Specify the spice host or UNIX socket [current: %s]\n" " -p PORT Specify the spice port or 0 for UNIX socket [current: %d]\n" " -j Disable cursor position scaling\n" " -M Don't hide the host cursor\n" "\n" " -K Set the FPS limit [current: %d]\n" " -k Enable FPS display\n" " -g NAME Force the use of a specific renderer\n" " -o OPTION Specify a renderer option (ie: opengl:vsync=0)\n" " Alternatively specify \"list\" to list all renderers and their options\n" "\n" " -a Auto resize the window to the guest\n" " -n Don't allow the window to be manually resized\n" " -r Don't maintain the aspect ratio\n" " -d Borderless mode\n" " -F Borderless fullscreen mode\n" " -x XPOS Initial window X position [current: %s]\n" " -y YPOS Initial window Y position [current: %s]\n" " -w WIDTH Initial window width [current: %u]\n" " -b HEIGHT Initial window height [current: %u]\n" " -Q Ignore requests to quit (ie: Alt+F4)\n" " -S Disable the screensaver\n" " -G Don't capture the keyboard in capture mode\n" " -m CODE Specify the capture key [current: %u (%s)]\n" " See https://wiki.libsdl.org/SDLScancodeLookup for valid values\n" " -q Disable alert messages [current: %s]\n" "\n" " -l License information\n" "\n", app, app, params.shmFile, params.shmSize, params.spiceHost, params.spicePort, params.fpsLimit, params.center ? "center" : x, params.center ? "center" : y, params.w, params.h, params.captureKey, params.disableAlerts ? "disabled" : "enabled", SDL_GetScancodeName(params.captureKey) ); } void doLicense() { fprintf(stderr, "\n" "Looking Glass - KVM FrameRelay (KVMFR) Client\n" "Copyright(C) 2017-2019 Geoffrey McRae \n" "https://looking-glass.hostfission.com\n" "\n" "This program is free software; you can redistribute it and / or modify it under\n" "the terms of the GNU General Public License as published by the Free Software\n" "Foundation; either version 2 of the License, or (at your option) any later\n" "version.\n" "\n" "This program is distributed in the hope that it will be useful, but WITHOUT ANY\n" "WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A\n" "PARTICULAR PURPOSE.See the GNU General Public License for more details.\n" "\n" "You should have received a copy of the GNU General Public License along with\n" "this program; if not, write to the Free Software Foundation, Inc., 59 Temple\n" "Place, Suite 330, Boston, MA 02111 - 1307 USA\n" "\n" ); } static bool load_config(const char * configFile) { config_t cfg; int itmp; const char *stmp; config_init(&cfg); if (!config_read_file(&cfg, configFile)) { DEBUG_ERROR("Config file error %s:%d - %s", config_error_file(&cfg), config_error_line(&cfg), config_error_text(&cfg) ); return false; } config_setting_t * global = config_lookup(&cfg, "global"); if (global) { if (config_setting_lookup_string(global, "shmFile", &stmp)) { free(params.shmFile); params.shmFile = strdup(stmp); } if (config_setting_lookup_int(global, "shmSize", &itmp)) params.shmSize = itmp * 1024 * 1024; if (config_setting_lookup_string(global, "forceRenderer", &stmp)) { bool ok = false; for(unsigned int i = 0; i < LG_RENDERER_COUNT; ++i) if (strcasecmp(LG_Renderers[i]->get_name(), stmp) == 0) { params.forceRenderer = true; params.forceRendererIndex = i; ok = true; break; } if (!ok) { DEBUG_ERROR("No such renderer: %s", stmp); config_destroy(&cfg); return false; } } if (config_setting_lookup_bool(global, "scaleMouseInput" , &itmp)) params.scaleMouseInput = (itmp != 0); if (config_setting_lookup_bool(global, "hideMouse" , &itmp)) params.hideMouse = (itmp != 0); if (config_setting_lookup_bool(global, "showFPS" , &itmp)) params.showFPS = (itmp != 0); if (config_setting_lookup_bool(global, "autoResize" , &itmp)) params.autoResize = (itmp != 0); if (config_setting_lookup_bool(global, "allowResize" , &itmp)) params.allowResize = (itmp != 0); if (config_setting_lookup_bool(global, "keepAspect" , &itmp)) params.keepAspect = (itmp != 0); if (config_setting_lookup_bool(global, "borderless" , &itmp)) params.borderless = (itmp != 0); if (config_setting_lookup_bool(global, "fullScreen" , &itmp)) params.fullscreen = (itmp != 0); if (config_setting_lookup_bool(global, "ignoreQuit" , &itmp)) params.ignoreQuit = (itmp != 0); if (config_setting_lookup_bool(global, "allowScreensaver", &itmp)) params.allowScreensaver = (itmp != 0); if (config_setting_lookup_bool(global, "disableAlerts" , &itmp)) params.disableAlerts = (itmp != 0); if (config_setting_lookup_int(global, "x", ¶ms.x)) params.center = false; if (config_setting_lookup_int(global, "y", ¶ms.y)) params.center = false; if (config_setting_lookup_int(global, "w", &itmp)) { if (itmp < 1) { DEBUG_ERROR("Invalid window width, must be greater then 1px"); config_destroy(&cfg); return false; } params.w = (unsigned int)itmp; } if (config_setting_lookup_int(global, "h", &itmp)) { if (itmp < 1) { DEBUG_ERROR("Invalid window height, must be greater then 1px"); config_destroy(&cfg); return false; } params.h = (unsigned int)itmp; } if (config_setting_lookup_int(global, "fpsLimit", &itmp)) { if (itmp < 1) { DEBUG_ERROR("Invalid FPS limit, must be greater then 0"); config_destroy(&cfg); return false; } params.fpsLimit = (unsigned int)itmp; } if (config_setting_lookup_int(global, "captureKey", &itmp)) { if (itmp <= SDL_SCANCODE_UNKNOWN || itmp > SDL_SCANCODE_APP2) { DEBUG_ERROR("Invalid capture key value, see https://wiki.libsdl.org/SDLScancodeLookup"); config_destroy(&cfg); return false; } params.captureKey = (SDL_Scancode)itmp; } } config_setting_t * spice = config_lookup(&cfg, "spice"); if (spice) { if (config_setting_lookup_bool(spice, "use", &itmp)) params.useSpice = (itmp != 0); if (config_setting_lookup_string(spice, "host", &stmp)) { free(params.spiceHost); params.spiceHost = strdup(stmp); } if (config_setting_lookup_int(spice, "port", &itmp)) { if (itmp < 0 || itmp > 65535) { DEBUG_ERROR("Invalid spice port"); config_destroy(&cfg); return false; } params.spicePort = itmp; } } for(unsigned int i = 0; i < LG_RENDERER_COUNT; ++i) { const LG_Renderer * r = LG_Renderers[i]; RendererOpts * opts = ¶ms.rendererOpts[i]; config_setting_t * group = config_lookup(&cfg, r->get_name()); if (!group) continue; for(unsigned int j = 0; j < r->option_count; ++j) { const char * name = r->options[j].name; if (!config_setting_lookup_string(group, name, &stmp)) continue; if (r->options[j].validator && !r->options[j].validator(stmp)) { DEBUG_ERROR("Renderer \"%s\" reported invalid value for option \"%s\"", r->get_name(), name); config_destroy(&cfg); return false; } if (opts->argc == opts->size) { opts->size += 5; opts->argv = realloc(opts->argv, sizeof(LG_RendererOptValue) * opts->size); } opts->argv[opts->argc].opt = &r->options[j]; opts->argv[opts->argc].value = strdup(stmp); ++opts->argc; } } config_destroy(&cfg); return true; } int main(int argc, char * argv[]) { params.shmFile = strdup(params.shmFile ); params.spiceHost = strdup(params.spiceHost); { // load any global then local config options first struct stat st; if (stat("/etc/looking-glass.conf", &st) >= 0) { DEBUG_INFO("Loading config from: /etc/looking-glass.conf"); if (!load_config("/etc/looking-glass.conf")) return -1; } struct passwd * pw = getpwuid(getuid()); const char pattern[] = "%s/.looking-glass.conf"; const size_t len = strlen(pw->pw_dir) + sizeof(pattern); char buffer[len]; snprintf(buffer, len, pattern, pw->pw_dir); if (stat(buffer, &st) >= 0) { DEBUG_INFO("Loading config from: %s", buffer); if (!load_config(buffer)) return -1; } } for(;;) { switch(getopt(argc, argv, "hC:f:L:sc:p:jMvK:kg:o:anrdFx:y:w:b:QSGm:lq")) { case '?': case 'h': default : doHelp(argv[0]); return -1; case -1: break; case 'C': params.configFile = optarg; if (!load_config(optarg)) return -1; continue; case 'f': free(params.shmFile); params.shmFile = strdup(optarg); continue; case 'L': params.shmSize = atoi(optarg) * 1024 * 1024; continue; case 's': params.useSpice = false; continue; case 'c': free(params.spiceHost); params.spiceHost = strdup(optarg); continue; case 'p': params.spicePort = atoi(optarg); continue; case 'j': params.scaleMouseInput = false; continue; case 'M': params.hideMouse = false; continue; case 'K': params.fpsLimit = atoi(optarg); continue; case 'k': params.showFPS = true; continue; case 'g': { bool ok = false; for(unsigned int i = 0; i < LG_RENDERER_COUNT; ++i) if (strcasecmp(LG_Renderers[i]->get_name(), optarg) == 0) { params.forceRenderer = true; params.forceRendererIndex = i; ok = true; break; } if (!ok) { fprintf(stderr, "No such renderer: %s\n", optarg); fprintf(stderr, "Use '-o list' obtain a list of options\n"); doHelp(argv[0]); return -1; } continue; } case 'o': { if (strcasecmp(optarg, "list") == 0) { size_t maxLen = 0; for(unsigned int i = 0; i < LG_RENDERER_COUNT; ++i) { const LG_Renderer * r = LG_Renderers[i]; for(unsigned int j = 0; j < r->option_count; ++j) { const size_t len = strlen(r->options[j].name); if (len > maxLen) maxLen = len; } } fprintf(stderr, "\nRenderer Option List\n"); for(unsigned int i = 0; i < LG_RENDERER_COUNT; ++i) { const LG_Renderer * r = LG_Renderers[i]; fprintf(stderr, "\n%s\n", r->get_name()); for(unsigned int j = 0; j < r->option_count; ++j) { const size_t pad = maxLen - strlen(r->options[j].name); for(int i = 0; i < pad; ++i) fputc(' ', stderr); fprintf(stderr, " %s - %s\n", r->options[j].name, r->options[j].desc); } } fprintf(stderr, "\n"); return -1; } const LG_Renderer * renderer = NULL; RendererOpts * opts = NULL; const size_t len = strlen(optarg); const char * name = strtok(optarg, ":"); for(unsigned int i = 0; i < LG_RENDERER_COUNT; ++i) if (strcasecmp(LG_Renderers[i]->get_name(), name) == 0) { renderer = LG_Renderers[i]; opts = ¶ms.rendererOpts[i]; break; } if (!renderer) { fprintf(stderr, "No such renderer: %s\n", name); doHelp(argv[0]); return -1; } const char * option = strtok(NULL , "="); if (!option) { fprintf(stderr, "Renderer option name not specified\n"); doHelp(argv[0]); return -1; } const LG_RendererOpt * opt = NULL; for(unsigned int i = 0; i < renderer->option_count; ++i) if (strcasecmp(option, renderer->options[i].name) == 0) { opt = &renderer->options[i]; break; } if (!opt) { fprintf(stderr, "Renderer \"%s\" doesn't have the option: %s\n", renderer->get_name(), option); doHelp(argv[0]); return -1; } const char * value = NULL; if (len > strlen(name) + strlen(option) + 2) value = option + strlen(option) + 1; if (opt->validator && !opt->validator(value)) { fprintf(stderr, "Renderer \"%s\" reported invalid value for option \"%s\"\n", renderer->get_name(), option); doHelp(argv[0]); return -1; } if (opts->argc == opts->size) { opts->size += 5; opts->argv = realloc(opts->argv, sizeof(LG_RendererOptValue) * opts->size); } opts->argv[opts->argc].opt = opt; opts->argv[opts->argc].value = strdup(value); ++opts->argc; continue; } case 'a': params.autoResize = true; continue; case 'n': params.allowResize = false; continue; case 'r': params.keepAspect = false; continue; case 'd': params.borderless = true; continue; case 'F': params.fullscreen = true; continue; case 'x': params.center = false; params.x = atoi(optarg); continue; case 'y': params.center = false; params.y = atoi(optarg); continue; case 'w': params.w = atoi(optarg); continue; case 'b': params.h = atoi(optarg); continue; case 'Q': params.ignoreQuit = true; continue; case 'S': params.allowScreensaver = false; continue; case 'G': params.grabKeyboard = false; continue; case 'm': params.captureKey = atoi(optarg); continue; case 'q': params.disableAlerts = true; continue; case 'l': doLicense(); return 0; } break; } if (optind != argc) { fprintf(stderr, "A non option was supplied\n"); doHelp(argv[0]); return -1; } if (params.grabKeyboard) { SDL_SetHint(SDL_HINT_GRAB_KEYBOARD, "1"); } const int ret = run(); free(params.shmFile); free(params.spiceHost); for(unsigned int i = 0; i < LG_RENDERER_COUNT; ++i) { RendererOpts * opts = ¶ms.rendererOpts[i]; for(unsigned int j = 0; j < opts->argc; ++j) free(opts->argv[j].value); free(opts->argv); } return ret; }