|
|
Previous NeXT emulator
/*
Hatari - control.c
This file is distributed under the GNU Public License, version 2 or at
your option any later version. Read the file gpl.txt for details.
This code processes commands from the Hatari control socket
*/
const char Control_fileid[] = "Hatari control.c : " __DATE__ " " __TIME__;
#include "config.h"
#if HAVE_UNIX_DOMAIN_SOCKETS
# include <sys/socket.h>
# include <sys/un.h>
#endif
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <ctype.h>
#include "main.h"
#include "change.h"
#include "configuration.h"
#include "control.h"
#include "debugui.h"
#include "file.h"
#include "log.h"
#include "screen.h"
#include "shortcut.h"
#include "str.h"
typedef enum {
DO_DISABLE,
DO_ENABLE,
DO_TOGGLE
} action_t;
/* Whether to send embedded window info */
static bool bSendEmbedInfo;
/* Pausing triggered remotely (battery save pause) */
static bool bRemotePaused;
/*-----------------------------------------------------------------------*/
/**
* Parse key command and synthetize key press/release
* corresponding to given keycode or character.
* Return false if parsing failed, true otherwise
*
* This can be used by external Hatari UI(s) for
* string macros, or on devices which lack keyboard
*/
static bool Control_InsertKey(const char *event)
{
const char *key = NULL;
if (strncmp(event, "keypress ", 9) == 0) {
key = &event[9];
} else if (strncmp(event, "keydown ", 8) == 0) {
key = &event[8];
} else if (strncmp(event, "keyup ", 6) == 0) {
key = &event[6];
}
if (!(key && key[0])) {
fprintf(stderr, "ERROR: '%s' contains no key press/down/up event\n", event);
return false;
}
if (key[1]) {
char *endptr;
/* multiple characters, assume it's a keycode */
int keycode = strtol(key, &endptr, 0);
/* not a valid number or keycode is out of range? */
if (*endptr || keycode < 0 || keycode > 255) {
fprintf(stderr, "ERROR: '%s' is not valid key scancode, got %d\n",
key, keycode);
return false;
}
}
#if 0
fprintf(stderr, "Simulated key %s of %d\n",
(down? (up? "press":"down") :"up"), key);
#endif
return true;
}
/*-----------------------------------------------------------------------*/
/**
* Parse event name and synthetize corresponding event to emulation
* Return false if name parsing failed, true otherwise
*
* This can be used by external Hatari UI(s) on devices which input
* methods differ from normal keyboard and mouse, such as high DPI
* touchscreen (no right/middle button, inaccurate clicks)
*/
static bool Control_InsertEvent(const char *event)
{
if (strcmp(event, "doubleclick") == 0) {
// Keyboard.LButtonDblClk = 1;
return true;
}
if (strcmp(event, "rightdown") == 0) {
// Keyboard.bRButtonDown |= BUTTON_MOUSE;
return true;
}
if (strcmp(event, "rightup") == 0) {
// Keyboard.bRButtonDown &= ~BUTTON_MOUSE;
return true;
}
if (Control_InsertKey(event)) {
return true;
}
fprintf(stderr, "ERROR: unrecognized event: '%s'\n", event);
fprintf(stderr,
"Supported mouse button and key events are:\n"
"- doubleclick\n"
"- rightdown\n"
"- rightup\n"
"- keypress <key>\n"
"- keydown <key>\n"
"- keyup <key>\n"
"<key> can be either a single ASCII character or an ST scancode\n"
"(e.g. space has scancode of 57 and enter 28).\n"
);
return false;
}
/*-----------------------------------------------------------------------*/
/**
* Parse device name and enable/disable/toggle & init/uninit it according
* to action. Return false if name parsing failed, true otherwise
*/
static bool Control_DeviceAction(const char *name, action_t action)
{
/* Note: e.g. RTC would require restarting emulation
* and HD-boot setting emulation reboot. Devices
* listed here work just with init/uninit.
*/
struct {
const char *name;
bool *pvalue;
void(*init)(void);
void(*uninit)(void);
} item[] = {
// { "printer", &ConfigureParams.Printer.bEnablePrinting, Printer_Init, Printer_UnInit },
// { "rs232", &ConfigureParams.RS232.bEnableRS232, RS232_Init, RS232_UnInit },
// { "midi", &ConfigureParams.Midi.bEnableMidi, Midi_Init, Midi_UnInit },
{ NULL, NULL, NULL, NULL }
};
int i;
bool value;
for (i = 0; item[i].name; i++)
{
if (strcmp(name, item[i].name) == 0)
{
switch (action) {
case DO_TOGGLE:
value = !*(item[i].pvalue);
break;
case DO_ENABLE:
value = true;
break;
case DO_DISABLE:
default:
value = false;
break;
}
*(item[i].pvalue) = value;
if (value) {
item[i].init();
} else {
item[i].uninit();
}
fprintf(stderr, "%s: %s\n", name, value?"ON":"OFF");
return true;
}
}
fprintf(stderr, "WARNING: unknown device '%s'\n\n", name);
fprintf(stderr, "Accepted devices are:\n");
for (i = 0; item[i].name; i++)
{
fprintf(stderr, "- %s\n", item[i].name);
}
return false;
}
/*-----------------------------------------------------------------------*/
/**
* Parse path type name and set the path to given value.
* Return false if name parsing failed, true otherwise
*/
static bool Control_SetPath(char *name)
{
struct {
const char *name;
char *path;
} item[] = {
{ NULL, NULL }
};
int i;
char *arg;
const char *value;
/* argument? */
arg = strchr(name, ' ');
if (arg) {
*arg = '\0';
value = Str_Trim(arg+1);
} else {
return false;
}
for (i = 0; item[i].name; i++)
{
if (strcmp(name, item[i].name) == 0)
{
fprintf(stderr, "%s: %s -> %s\n", name, item[i].path, value);
strncpy(item[i].path, value, FILENAME_MAX-1);
return true;
}
}
fprintf(stderr, "WARNING: unknown path type '%s'\n\n", name);
fprintf(stderr, "Accepted paths types are:\n");
for (i = 0; item[i].name; i++)
{
fprintf(stderr, "- %s\n", item[i].name);
}
return false;
}
/*-----------------------------------------------------------------------*/
/**
* Show Hatari remote usage info and return false
*/
static bool Control_Usage(const char *cmd)
{
fprintf(stderr, "ERROR: unrecognized hatari command: '%s'", cmd);
fprintf(stderr,
"Supported commands are:\n"
"- hatari-debug <Debug UI command>\n"
"- hatari-event <event to simulate>\n"
"- hatari-option <command line options>\n"
"- hatari-enable/disable/toggle <device name>\n"
"- hatari-path <config name> <new path>\n"
"- hatari-shortcut <shortcut name>\n"
"- hatari-embed-info\n"
"- hatari-stop\n"
"- hatari-cont\n"
"The last two can be used to stop and continue the Hatari emulation.\n"
"All commands need to be separated by newlines.\n"
);
return false;
}
/*-----------------------------------------------------------------------*/
/**
* Parse Hatari debug/event/option/toggle/path/shortcut command buffer.
* Given buffer is modified in-place.
*/
void Control_ProcessBuffer(char *buffer)
{
char *cmd, *cmdend, *arg;
int ok = true;
cmd = buffer;
do {
/* command terminator? */
cmdend = strchr(cmd, '\n');
if (cmdend) {
*cmdend = '\0';
}
/* arguments? */
arg = strchr(cmd, ' ');
if (arg) {
*arg = '\0';
arg = Str_Trim(arg+1);
}
if (arg) {
if (strcmp(cmd, "hatari-option") == 0) {
ok = Change_ApplyCommandline(arg);
} else if (strcmp(cmd, "hatari-debug") == 0) {
ok = DebugUI_RemoteParse(arg);
} else if (strcmp(cmd, "hatari-shortcut") == 0) {
ok = Shortcut_Invoke(arg);
} else if (strcmp(cmd, "hatari-event") == 0) {
ok = Control_InsertEvent(arg);
} else if (strcmp(cmd, "hatari-path") == 0) {
ok = Control_SetPath(arg);
} else if (strcmp(cmd, "hatari-enable") == 0) {
ok = Control_DeviceAction(arg, DO_ENABLE);
} else if (strcmp(cmd, "hatari-disable") == 0) {
ok = Control_DeviceAction(arg, DO_DISABLE);
} else if (strcmp(cmd, "hatari-toggle") == 0) {
ok = Control_DeviceAction(arg, DO_TOGGLE);
} else {
ok = Control_Usage(cmd);
}
} else {
if (strcmp(cmd, "hatari-embed-info") == 0) {
fprintf(stderr, "Embedded window ID change messages = ON\n");
bSendEmbedInfo = true;
} else if (strcmp(cmd, "hatari-stop") == 0) {
Main_PauseEmulation(true);
bRemotePaused = true;
} else if (strcmp(cmd, "hatari-cont") == 0) {
Main_UnPauseEmulation();
bRemotePaused = false;
} else {
ok = Control_Usage(cmd);
}
}
if (cmdend) {
cmd = cmdend + 1;
}
} while (ok && cmdend && *cmd);
}
#if HAVE_UNIX_DOMAIN_SOCKETS
/* socket from which control command line options are read */
static int ControlSocket;
/* pre-declared local functions */
static int Control_GetUISocket(void);
/*-----------------------------------------------------------------------*/
/**
* Check ControlSocket for new commands and execute them.
* Commands should be separated by newlines.
*
* Return true if remote pause ON (and connected), false otherwise
*/
bool Control_CheckUpdates(void)
{
/* just using all trace options with +/- are about 300 chars */
char buffer[400];
struct timeval tv;
fd_set readfds;
ssize_t bytes;
int status, sock;
/* socket of file? */
if (ControlSocket) {
sock = ControlSocket;
} else {
return false;
}
/* ready for reading? */
tv.tv_usec = tv.tv_sec = 0;
do {
FD_ZERO(&readfds);
FD_SET(sock, &readfds);
if (bRemotePaused) {
/* return only when there're UI events
* (redraws etc) to save battery:
* http://bugzilla.libsdl.org/show_bug.cgi?id=323
*/
int uisock = Control_GetUISocket();
if (uisock) {
FD_SET(uisock, &readfds);
if (uisock < sock) {
uisock = sock;
}
}
status = select(uisock+1, &readfds, NULL, NULL, NULL);
} else {
status = select(sock+1, &readfds, NULL, NULL, &tv);
}
if (status < 0) {
perror("Control socket select() error");
return false;
}
/* nothing to process here */
if (status == 0) {
return bRemotePaused;
}
if (!FD_ISSET(sock, &readfds)) {
return bRemotePaused;
}
/* assume whole command can be read in one go */
bytes = read(sock, buffer, sizeof(buffer)-1);
if (bytes < 0)
{
perror("Control socket read");
return false;
}
if (bytes == 0) {
/* closed */
close(ControlSocket);
ControlSocket = 0;
return false;
}
buffer[bytes] = '\0';
Control_ProcessBuffer(buffer);
} while (bRemotePaused);
return false;
}
/*-----------------------------------------------------------------------*/
/**
* Open given control socket.
* Return NULL for success, otherwise an error string
*/
const char *Control_SetSocket(const char *socketpath)
{
struct sockaddr_un address;
int newsock;
newsock = socket(AF_UNIX, SOCK_STREAM, 0);
if (newsock < 0)
{
perror("socket creation");
return "Can't create AF_UNIX socket";
}
address.sun_family = AF_UNIX;
strncpy(address.sun_path, socketpath, sizeof(address.sun_path));
address.sun_path[sizeof(address.sun_path)-1] = '\0';
Log_Printf(LOG_INFO, "Connecting to control socket '%s'...\n", address.sun_path);
if (connect(newsock, (struct sockaddr *)&address, sizeof(address)) < 0)
{
perror("socket connect");
close(newsock);
return "connection to control socket failed";
}
if (ControlSocket) {
close(ControlSocket);
}
ControlSocket = newsock;
Log_Printf(LOG_INFO, "new control socket is '%s'\n", socketpath);
return NULL;
}
/*-----------------------------------------------------------------------
* Currently works only on X11.
*
* SDL_syswm.h automatically includes everything else needed.
*/
#if HAVE_SDL_SDL_CONFIG_H
#include <SDL_config.h>
#endif
/* X11 available and SDL_config.h states that SDL supports X11 */
#if HAVE_X11 && SDL_VIDEO_DRIVER_X11
#include <SDL_syswm.h>
/**
* Reparent Hatari window if so requested. Needs to be done inside
* Hatari because if SDL itself is requested to reparent itself,
* SDL window stops accepting any input (specifically done like
* this in SDL backends for some reason).
*
* 'noembed' argument tells whether the SDL window should be embedded
* or not.
*
* If the window is embedded (which means that SDL WM window needs
* to be hidden) when SDL is asked to fullscreen, Hatari window just
* disappears when returning back from fullscreen. I.e. call this
* with noembed=true _before_ fullscreening and any other time with
* noembed=false after changing window size. You can do this by
* giving bInFullscreen as the noembed value.
*/
void Control_ReparentWindow(int width, int height, bool noembed)
{
#if 0
Display *display;
Window parent_win, sdl_win, wm_win;
const char *parent_win_id;
SDL_SysWMinfo info;
parent_win_id = getenv("PARENT_WIN_ID");
if (!parent_win_id) {
return;
}
parent_win = strtol(parent_win_id, NULL, 0);
if (!parent_win) {
Log_Printf(LOG_WARN, "Invalid PARENT_WIN_ID value '%s'\n", parent_win_id);
return;
}
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(sdlWindow, &info)) {
Log_Printf(LOG_WARN, "Failed to get SDL_GetWMInfo()\n");
return;
}
display = info.info.x11.display;
sdl_win = info.info.x11.window;
wm_win = info.info.x11.wmwindow;
info.info.x11.lock_func();
if (noembed) {
/* show WM window again */
XMapWindow(display, wm_win);
} else {
char buffer[12]; /* 32-bits in hex (+ '\r') + '\n' + '\0' */
/* hide WM window for Hatari */
XUnmapWindow(display, wm_win);
/* reparent main Hatari window to given parent */
XReparentWindow(display, sdl_win, parent_win, 0, 0);
/* whether to send new window size */
if (bSendEmbedInfo && ControlSocket) {
fprintf(stderr, "New %dx%d SDL window with ID: %lx\n",
width, height, sdl_win);
sprintf(buffer, "%dx%d", width, height);
if (write(ControlSocket, buffer, strlen(buffer)) < 0)
perror("Control_ReparentWindow write");
}
}
info.info.x11.unlock_func();
#else
fprintf(stderr, "FIXME: Port Control_ReparentWindow to SDL2\n");
#endif
}
/**
* Return the X connection socket or zero
*/
static int Control_GetUISocket(void)
{
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(sdlWindow, &info)) {
Log_Printf(LOG_WARN, "Failed to get SDL_GetWMInfo()\n");
return 0;
}
return ConnectionNumber(info.info.x11.display);
}
#else /* HAVE_X11 */
static int Control_GetUISocket(void)
{
return 0;
}
void Control_ReparentWindow(int width, int height, bool noembed)
{
/* TODO: implement the Windows part. SDL sources offer example */
Log_Printf(LOG_TODO, "Support for Hatari window reparenting not built in\n");
}
#endif /* HAVE_X11 */
#endif /* HAVE_UNIX_DOMAIN_SOCKETS */
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.