OXYGENHUB
◆ main openmsx / Contrib / openmsx-control-socket.cc
10.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
/**
2
 * Example implementation for bidirectional communication with openMSX.
3
 *
4
 *  requires: libxml2
5
 *  compile:
6
 *    *nix:  g++ -std=c++23 `xml2-config --cflags` `xml2-config --libs` openmsx-control-socket.cc
7
 *    win32: g++ -std=c++23 `xml2-config --cflags` `xml2-config --libs` openmsx-control-socket.cc -lwsock32
8
 */
9
10
#include <libxml/parser.h>
11
12
#include <array>
13
#include <deque>
14
#include <dirent.h>
15
#include <iostream>
16
#include <memory>
17
#include <string>
18
#include <sys/select.h>
19
#include <sys/stat.h>
20
#include <sys/types.h>
21
#include <unistd.h>
22
#include <unistd.h>
23
#include <vector>
24
25
#ifdef _WIN32
26
#include <fstream>
27
#include <winsock2.h>
28
#else
29
#include <sys/socket.h>
30
#include <netinet/in.h>
31
#include <sys/un.h>
32
#include <pwd.h>
33
#endif
34
35
36
class ReadDir
37
{
38
public:
39
	explicit ReadDir(const std::string& directory)
40
		: dir(opendir(directory.c_str())) {}
41
42
	dirent* getEntry() {
43
		if (!dir) return nullptr;
44
		return readdir(dir.get());
45
	}
46
47
private:
48
	struct CloseDir {
49
		void operator()(DIR* d) const { if (d) closedir(d); }
50
	};
51
	using DIR_t = std::unique_ptr<DIR, CloseDir>;
52
	DIR_t dir;
53
};
54
55
static std::string getTempDir()
56
{
57
	const char* result = nullptr;
58
	if (!result) result = getenv("TMPDIR");
59
	if (!result) result = getenv("TMP");
60
	if (!result) result = getenv("TEMP");
61
	if (!result) {
62
#ifdef _WIN32
63
		result = "C:/WINDOWS/TEMP";
64
#else
65
		result = "/tmp";
66
#endif
67
	}
68
	return result;
69
}
70
71
static std::string getUserName()
72
{
73
#ifdef _WIN32
74
	return "default";
75
#else
76
	const struct passwd* pw = getpwuid(getuid());
77
	return pw->pw_name ? pw->pw_name : "";
78
#endif
79
}
80
81
82
class OpenMSXComm
83
{
84
public:
85
	// main loop
86
	void start(int sd);
87
88
	// send a command to openmsx
89
	void sendCommand(const std::string& command);
90
91
private:
92
	// XML parsing call-back functions
93
	static void cb_start_element(OpenMSXComm* comm, const xmlChar* name,
94
	                             const xmlChar** attrs);
95
	static void cb_end_element(OpenMSXComm* comm, const xmlChar* name);
96
	static void cb_text(OpenMSXComm* comm, const xmlChar* chars, int len);
97
98
	void parseReply(const char** attrs);
99
	void parseLog(const char** attrs);
100
	void parseUpdate(const char** attrs);
101
102
	void doReply();
103
	void doLog() const;
104
	void doUpdate() const;
105
106
	// commands being executed
107
	std::deque<std::string> commandStack;
108
109
	// XML parsing
110
	enum State {
111
		START,
112
		TAG_OPENMSX,
113
		TAG_REPLY,
114
		TAG_LOG,
115
		TAG_UPDATE,
116
	} state;
117
	unsigned unknownLevel;
118
	std::string content;
119
	xmlSAXHandler sax_handler;
120
	xmlParserCtxt* parser_context;
121
122
	enum ReplyStatus {
123
		REPLY_UNKNOWN,
124
		REPLY_OK,
125
		REPLY_NOK
126
	} replyStatus;
127
128
	enum LogLevel {
129
		LOG_UNKNOWN,
130
		LOG_INFO,
131
		LOG_WARNING
132
	} logLevel;
133
134
	std::string updateType;
135
	std::string updateName;
136
137
	// communication with openmsx process
138
	int sd;
139
};
140
141
142
void OpenMSXComm::cb_start_element(OpenMSXComm* comm, const xmlChar* name,
143
                                   const xmlChar** attrs)
144
{
145
	if (comm->unknownLevel) {
146
		++(comm->unknownLevel);
147
		return;
148
	}
149
	switch (comm->state) {
150
		case START:
151
			if (strcmp((const char*)name, "openmsx-output") == 0) {
152
				comm->state = TAG_OPENMSX;
153
			} else {
154
				++(comm->unknownLevel);
155
			}
156
			break;
157
		case TAG_OPENMSX:
158
			if (strcmp((const char*)name, "reply") == 0) {
159
				comm->state = TAG_REPLY;
160
				comm->parseReply((const char**)attrs);
161
			} else if (strcmp((const char*)name, "log") == 0) {
162
				comm->state = TAG_LOG;
163
				comm->parseLog((const char**)attrs);
164
			} else if (strcmp((const char*)name, "update") == 0) {
165
				comm->state = TAG_UPDATE;
166
				comm->parseUpdate((const char**)attrs);
167
			} else {
168
				++(comm->unknownLevel);
169
			}
170
			break;
171
		default:
172
			++(comm->unknownLevel);
173
			break;
174
	}
175
	comm->content.clear();
176
}
177
178
void OpenMSXComm::parseReply(const char** attrs)
179
{
180
	replyStatus = REPLY_UNKNOWN;
181
	if (!attrs) return;
182
	for ( ; *attrs; attrs += 2) {
183
		if (strcmp(attrs[0], "result") == 0) {
184
			if (strcmp(attrs[1], "ok") == 0) {
185
				replyStatus = REPLY_OK;
186
			} else if (strcmp(attrs[1], "nok") == 0) {
187
				replyStatus = REPLY_NOK;
188
			}
189
		}
190
	}
191
}
192
193
void OpenMSXComm::parseLog(const char** attrs)
194
{
195
	logLevel = LOG_UNKNOWN;
196
	if (!attrs) return;
197
	for ( ; *attrs; attrs += 2) {
198
		if (strcmp(attrs[0], "level") == 0) {
199
			if (strcmp(attrs[1], "info") == 0) {
200
				logLevel = LOG_INFO;
201
			} else if (strcmp(attrs[1], "warning") == 0) {
202
				logLevel = LOG_WARNING;
203
			}
204
		}
205
	}
206
}
207
208
void OpenMSXComm::parseUpdate(const char** attrs)
209
{
210
	updateType = "unknown";
211
	if (!attrs) return;
212
	for ( ; *attrs; attrs += 2) {
213
		if (strcmp(attrs[0], "type") == 0) {
214
			updateType = attrs[1];
215
		} else if (strcmp(attrs[0], "name") == 0) {
216
			updateName = attrs[1];
217
		}
218
	}
219
}
220
221
void OpenMSXComm::cb_end_element(OpenMSXComm* comm, const xmlChar* /*name*/)
222
{
223
	if (comm->unknownLevel) {
224
		--(comm->unknownLevel);
225
		return;
226
	}
227
	switch (comm->state) {
228
		case TAG_OPENMSX:
229
			comm->state = START;
230
			break;
231
		case TAG_REPLY:
232
			comm->doReply();
233
			comm->state = TAG_OPENMSX;
234
			break;
235
		case TAG_LOG:
236
			comm->doLog();
237
			comm->state = TAG_OPENMSX;
238
			break;
239
		case TAG_UPDATE:
240
			comm->doUpdate();
241
			comm->state = TAG_OPENMSX;
242
			break;
243
		default:
244
			break;
245
	}
246
}
247
248
void OpenMSXComm::doReply()
249
{
250
	switch (replyStatus) {
251
		case REPLY_OK:
252
			std::cout << "OK: ";
253
			break;
254
		case REPLY_NOK:
255
			std::cout << "ERR: ";
256
			break;
257
		case REPLY_UNKNOWN:
258
			// ignore
259
			break;
260
	}
261
	std::cout << commandStack.front() << '\n';
262
	commandStack.pop_front();
263
	if (!content.empty()) {
264
		std::cout << content << '\n';
265
	}
266
	std::cout << std::flush;
267
}
268
269
void OpenMSXComm::doLog() const
270
{
271
	switch (logLevel) {
272
		case LOG_INFO:
273
			std::cout << "INFO: ";
274
			break;
275
		case LOG_WARNING:
276
			std::cout << "WARNING: ";
277
			break;
278
		case LOG_UNKNOWN:
279
			// ignore
280
			break;
281
	}
282
	std::cout << content << '\n' << std::flush;
283
}
284
285
void OpenMSXComm::doUpdate() const
286
{
287
	std::cout << "UPDATE: " << updateType << " " << updateName << " " << content << '\n' << std::flush;
288
}
289
290
void OpenMSXComm::cb_text(OpenMSXComm* comm, const xmlChar* chars, int len)
291
{
292
	switch (comm->state) {
293
		case TAG_REPLY:
294
		case TAG_LOG:
295
		case TAG_UPDATE:
296
			comm->content.append((const char*)chars, len);
297
			break;
298
		default:
299
			break;
300
	}
301
}
302
303
304
void OpenMSXComm::sendCommand(const std::string& command)
305
{
306
	write(sd, "<command>", 9);
307
	write(sd, command.c_str(), command.length());
308
	write(sd, "</command>", 10);
309
	commandStack.push_back(command);
310
}
311
312
void OpenMSXComm::start(int sd_)
313
{
314
	sd = sd_;
315
316
	// init XML parser
317
	state = START;
318
	unknownLevel = 0;
319
	memset(&sax_handler, 0, sizeof(sax_handler));
320
	sax_handler.startElement = (startElementSAXFunc)cb_start_element;
321
	sax_handler.endElement   = (endElementSAXFunc)  cb_end_element;
322
	sax_handler.characters   = (charactersSAXFunc)  cb_text;
323
	parser_context = xmlCreatePushParserCtxt(&sax_handler, this, nullptr, 0, nullptr);
324
325
	write(sd, "<openmsx-control>", 17);
326
327
	// event loop
328
	std::string command; // (partial) input from STDIN
329
	while (true) {
330
		std::array<char, 4096> buf;
331
		fd_set rdFs;
332
		FD_ZERO(&rdFs);
333
		FD_SET(sd, &rdFs);
334
		FD_SET(STDIN_FILENO, &rdFs);
335
		select(sd + 1, &rdFs, nullptr, nullptr, nullptr);
336
		if (FD_ISSET(sd, &rdFs)) {
337
			// data available from openMSX
338
			ssize_t size = read(sd, buf.data(), buf.size());
339
			if (size == 0) {
340
				// openmsx process died
341
				break;
342
			}
343
			xmlParseChunk(parser_context, buf.data(), size, 0);
344
		}
345
		if (FD_ISSET(STDIN_FILENO, &rdFs)) {
346
			// data available from STDIN
347
			ssize_t size = read(STDIN_FILENO, buf.data(), buf.size());
348
			char* oldPos = buf.data();
349
			while (true) {
350
				if (auto* pos = static_cast<char*>(memchr(oldPos, '\n', size))) {
351
					auto num = pos - oldPos;
352
					command.append(oldPos, num);
353
					sendCommand(command);
354
					command.clear();
355
					oldPos = pos + 1;
356
					size -= num + 1;
357
				} else {
358
					command.append(oldPos, size);
359
					break;
360
				}
361
			}
362
		}
363
	}
364
365
	// cleanup
366
	xmlFreeParserCtxt(parser_context);
367
}
368
369
370
static bool checkSocketDir(const std::string& dir)
371
{
372
	struct stat st;
373
	if (stat(dir.c_str(), &st)) {
374
		// cannot stat
375
		return false;
376
	}
377
	if (!S_ISDIR(st.st_mode)) {
378
		// not a directory
379
		return false;
380
	}
381
#ifndef _WIN32
382
	// only do permission and owner checks on *nix
383
	if ((st.st_mode & 0777) != 0700) {
384
		// wrong permissions
385
		return false;
386
	}
387
	if (st.st_uid != getuid()) {
388
		// wrong uid
389
		return false;
390
	}
391
#endif
392
	return true;
393
}
394
395
static bool checkSocket(const std::string& socket)
396
{
397
	std::string dir  = socket.substr(0, socket.find_last_of('/'));
398
	std::string name = socket.substr(socket.find_last_of('/') + 1);
399
400
	if (!name.starts_with("socket.")) {
401
		// wrong name
402
		return false;
403
	}
404
405
	struct stat st;
406
	if (stat(socket.c_str(), &st)) {
407
		// cannot stat
408
		return false;
409
	}
410
#ifdef _WIN32
411
	if (!S_ISREG(st.st_mode)) {
412
		// not a regular file
413
		return false;
414
	}
415
#else
416
	if (!S_ISSOCK(st.st_mode)) {
417
		// not a socket
418
		return false;
419
	}
420
#endif
421
#ifndef _WIN32
422
	// only do permission and owner checks on *nix
423
	if ((st.st_mode & 0777) != 0600) {
424
		// check will be different on win32 (!= 777) thus actually useless
425
		// wrong permissions
426
		return false;
427
	}
428
	if (st.st_uid != getuid()) {
429
		// does this work on win32? is this check meaningful?
430
		// wrong uid
431
		return false;
432
	}
433
#endif
434
	return true;
435
}
436
437
static void deleteSocket(const std::string& socket)
438
{
439
	unlink(socket.c_str()); // ignore errors
440
	std::string dir = socket.substr(0, socket.find_last_of('/'));
441
	rmdir(dir.c_str()); // ignore errors
442
}
443
444
static int openSocket(const std::string& socketName)
445
{
446
	if (!checkSocket(socketName)) {
447
		return -1;
448
	}
449
450
#ifdef _WIN32
451
	int port = -1;
452
	std::ifstream in(socketName.c_str());
453
	in >> port;
454
	if (port == -1) {
455
		return -1;
456
	}
457
458
	int sd = socket(AF_INET, SOCK_STREAM, 0);
459
	if (sd == -1) {
460
		return -1;
461
	}
462
463
	sockaddr_in addr;
464
	memset((char*)&addr, 0, sizeof(addr));
465
	addr.sin_family = AF_INET;
466
	addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
467
	addr.sin_port = htons(port);
468
469
#else
470
	int sd = socket(AF_UNIX, SOCK_STREAM, 0);
471
	if (sd == -1) {
472
		return -1;
473
	}
474
475
	sockaddr_un addr;
476
	addr.sun_family = AF_UNIX;
477
	strcpy(addr.sun_path, socketName.c_str());
478
#endif
479
480
	if (connect(sd, (sockaddr*)&addr, sizeof(addr)) == -1) {
481
		// It appears to be a socket but we cannot connect to it.
482
		// Must be a stale socket. Try to clean it up.
483
		deleteSocket(socketName);
484
		close(sd);
485
		return -1;
486
	}
487
488
	return sd;
489
}
490
491
void collectServers(std::vector<std::string>& servers)
492
{
493
	std::string dir = getTempDir() + "/openmsx-" + getUserName();
494
	if (!checkSocketDir(dir)) {
495
		return;
496
	}
497
	ReadDir readDir(dir);
498
	while (const dirent* entry = readDir.getEntry()) {
499
		std::string socketName = dir + '/' + entry->d_name;
500
		int sd = openSocket(socketName);
501
		if (sd != -1) {
502
			close(sd);
503
			servers.push_back(socketName);
504
		}
505
	}
506
}
507
508
509
int main()
510
{
511
#ifdef _WIN32
512
	WSAData wsaData;
513
	WSAStartup(MAKEWORD(1, 1), &wsaData);
514
#endif
515
516
	std::vector<std::string> servers;
517
	collectServers(servers);
518
	if (servers.empty()) {
519
		std::cout << "No running openmsx found.\n";
520
		return 0;
521
	}
522
523
	// TODO let the user pick one if there is more than 1
524
	int sd = openSocket(servers.front());
525
526
	OpenMSXComm comm;
527
	comm.start(sd);
528
	return 0;
529
}