OXYGENHUB
◆ main openmsx / src / events / CliConnection.cc
9.0 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
// TODO:
2
// - To avoid any possible conflicts, anything called from "run" should be
3
//   locked.
4
// - Maybe document for each method whether it is called from the listener
5
//   thread or from the main thread?
6
// - Unsubscribe at CliComm after stream is closed.
7
8
#include "CliConnection.hh"
9
10
#include "Event.hh"
11
#include "EventDistributor.hh"
12
13
#include "CommandController.hh"
14
#include "CommandException.hh"
15
#include "TclObject.hh"
16
#include "XMLEscape.hh"
17
18
#include "TemporaryString.hh"
19
#include "cstdiop.hh"
20
#include "ranges.hh"
21
#include "unistdp.hh"
22
23
#include <algorithm>
24
#include <array>
25
#include <cassert>
26
#include <iostream>
27
28
#ifdef _WIN32
29
#include "SocketStreamWrapper.hh"
30
#include "SspiNegotiateServer.hh"
31
#endif
32
33
namespace openmsx {
34
35
using namespace std::literals;
36
37
// class CliConnection
38
39
CliConnection::CliConnection(CommandController& commandController_,
40
                             EventDistributor& eventDistributor_)
41
	: parser([this](const std::string& cmd) { execute(cmd); })
42
	, commandController(commandController_)
43
	, eventDistributor(eventDistributor_)
44
{
45
	std::ranges::fill(updateEnabled, false);
46
47
	eventDistributor.registerEventListener(EventType::CLICOMMAND, *this);
48
}
49
50
CliConnection::~CliConnection()
51
{
52
	eventDistributor.unregisterEventListener(EventType::CLICOMMAND, *this);
53
}
54
55
void CliConnection::log(CliComm::LogLevel level, std::string_view message, float fraction) noexcept
56
{
57
	std::string fullMessage{message};
58
	if (level == CliComm::LogLevel::PROGRESS && fraction >= 0.0f) {
59
		strAppend(fullMessage, "... ", int(100.0f * fraction), '%');
60
	}
61
	output(tmpStrCat("<log level=\"", toString(level), "\">",
62
	                 XMLEscape(fullMessage), "</log>\n"));
63
}
64
65
void CliConnection::update(CliComm::UpdateType type, std::string_view machine,
66
                           std::string_view name, std::string_view value) noexcept
67
{
68
	if (!getUpdateEnable(type)) return;
69
70
	auto tmp = tmpStrCat(
71
		"<update type=\"", toString(type), '\"',
72
		strCat_if(!machine.empty(), " machine=\"", machine, '"'),
73
		strCat_if(!name.empty(), " name=\"", XMLEscape(name), '"'),
74
		'>', XMLEscape(value), "</update>\n");
75
	output(tmp);
76
}
77
78
void CliConnection::startOutput()
79
{
80
	output("<openmsx-output>\n");
81
}
82
83
void CliConnection::start()
84
{
85
	thread = std::thread([this]() { run(); });
86
}
87
88
void CliConnection::end()
89
{
90
	output("</openmsx-output>\n");
91
	close();
92
93
	poller.abort();
94
	// Thread might not be running if start() was never called.
95
	if (thread.joinable()) {
96
		thread.join();
97
	}
98
}
99
100
void CliConnection::execute(const std::string& command)
101
{
102
	eventDistributor.distributeEvent(CliCommandEvent(command, this));
103
}
104
105
static TemporaryString reply(std::string_view message, bool status)
106
{
107
	return tmpStrCat("<reply result=\"", (status ? "ok"sv : "nok"sv), "\">",
108
	                 XMLEscape(message), "</reply>\n");
109
}
110
111
bool CliConnection::signalEvent(const Event& event)
112
{
113
	assert(getType(event) == EventType::CLICOMMAND);
114
	if (const auto& commandEvent = get_event<CliCommandEvent>(event);
115
	    commandEvent.getId() == this) {
116
		try {
117
			auto result = commandController.executeCommand(
118
				commandEvent.getCommand(), this).getString();
119
			output(reply(result, true));
120
		} catch (CommandException& e) {
121
			std::string result = std::move(e).getMessage() + '\n';
122
			output(reply(result, false));
123
		}
124
	}
125
	return false;
126
}
127
128
129
// class StdioConnection
130
131
static constexpr int BUF_SIZE = 4096;
132
StdioConnection::StdioConnection(CommandController& commandController_,
133
                                 EventDistributor& eventDistributor_)
134
	: CliConnection(commandController_, eventDistributor_)
135
{
136
	startOutput();
137
}
138
139
StdioConnection::~StdioConnection()
140
{
141
	end();
142
}
143
144
void StdioConnection::run()
145
{
146
	// runs in helper thread
147
	while (true) {
148
#ifdef _WIN32
149
		if (poller.aborted()) break;
150
#else
151
		if (poller.poll(STDIN_FILENO)) break;
152
#endif
153
		std::array<char, BUF_SIZE> buf;
154
		auto n = read(STDIN_FILENO, buf.data(), sizeof(buf));
155
		if (n > 0) {
156
			parser.parse(subspan(buf, 0, n));
157
		} else if (n < 0) {
158
			break;
159
		}
160
	}
161
}
162
163
void StdioConnection::output(std::string_view message)
164
{
165
	std::cout << message << std::flush;
166
}
167
168
void StdioConnection::close()
169
{
170
	// don't close stdin/out/err
171
}
172
173
174
#ifdef _WIN32
175
// class PipeConnection
176
177
// INVALID_HANDLE_VALUE is #defined as  (HANDLE)(-1)
178
// but that gives a old-style-cast warning
179
static const HANDLE OPENMSX_INVALID_HANDLE_VALUE = reinterpret_cast<HANDLE>(-1);
180
181
PipeConnection::PipeConnection(CommandController& commandController_,
182
                               EventDistributor& eventDistributor_,
183
                               std::string_view name)
184
	: CliConnection(commandController_, eventDistributor_)
185
{
186
	auto pipeName = strCat("\\\\.\\pipe\\", name);
187
	pipeHandle = CreateFileA(pipeName.c_str(), GENERIC_READ, 0, nullptr,
188
	                         OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
189
	if (pipeHandle == OPENMSX_INVALID_HANDLE_VALUE) {
190
		throw FatalError("Error reopening pipefile '", pipeName, "': error ",
191
		                 unsigned(GetLastError()));
192
	}
193
194
	shutdownEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
195
	if (!shutdownEvent) {
196
		throw FatalError("Error creating shutdown event: ", GetLastError());
197
	}
198
199
	startOutput();
200
}
201
202
PipeConnection::~PipeConnection()
203
{
204
	end();
205
206
	assert(pipeHandle == OPENMSX_INVALID_HANDLE_VALUE);
207
	CloseHandle(shutdownEvent);
208
}
209
210
static void InitOverlapped(LPOVERLAPPED overlapped)
211
{
212
	ZeroMemory(overlapped, sizeof(*overlapped));
213
	overlapped->hEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
214
	if (!overlapped->hEvent) {
215
		throw FatalError("Error creating overlapped event: ", GetLastError());
216
	}
217
}
218
219
static void ClearOverlapped(LPOVERLAPPED overlapped)
220
{
221
	if (overlapped->hEvent) {
222
		CloseHandle(overlapped->hEvent);
223
		overlapped->hEvent = nullptr;
224
	}
225
}
226
227
void PipeConnection::run()
228
{
229
	// runs in helper thread
230
	OVERLAPPED overlapped;
231
	InitOverlapped(&overlapped);
232
	HANDLE waitHandles[2] = { shutdownEvent, overlapped.hEvent };
233
234
	while (pipeHandle != OPENMSX_INVALID_HANDLE_VALUE) {
235
		char buf[BUF_SIZE];
236
		if (!ReadFile(pipeHandle, buf, BUF_SIZE, nullptr, &overlapped) &&
237
			GetLastError() != ERROR_IO_PENDING) {
238
			break; // Pipe broke
239
		}
240
		DWORD wait = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE);
241
		if (wait == WAIT_OBJECT_0 + 1) {
242
			DWORD bytesRead;
243
			if (!GetOverlappedResult(pipeHandle, &overlapped, &bytesRead, TRUE)) {
244
				break; // Pipe broke
245
			}
246
			parser.parse(std::span{buf, bytesRead});
247
		} else if (wait == WAIT_OBJECT_0) {
248
			break; // Shutdown
249
		} else {
250
			throw FatalError(
251
				"WaitForMultipleObjects returned unexpectedly: ", wait);
252
		}
253
	}
254
255
	ClearOverlapped(&overlapped);
256
257
	// We own the pipe handle, so close it here
258
	CloseHandle(pipeHandle);
259
	pipeHandle = OPENMSX_INVALID_HANDLE_VALUE;
260
}
261
262
void PipeConnection::output(std::string_view message)
263
{
264
	if (pipeHandle != OPENMSX_INVALID_HANDLE_VALUE) {
265
		std::cout << message << std::flush;
266
	}
267
}
268
269
void PipeConnection::close()
270
{
271
	SetEvent(shutdownEvent);
272
}
273
#endif // _WIN32
274
275
276
// class SocketConnection
277
278
SocketConnection::SocketConnection(CommandController& commandController_,
279
                                   EventDistributor& eventDistributor_,
280
                                   SOCKET sd_)
281
	: CliConnection(commandController_, eventDistributor_)
282
	, sd(sd_)
283
{
284
}
285
286
SocketConnection::~SocketConnection()
287
{
288
	end();
289
}
290
291
void SocketConnection::run()
292
{
293
	// runs in helper thread
294
#ifdef _WIN32
295
	bool ok;
296
	{
297
		std::scoped_lock lock(sdMutex);
298
		// Authenticate and authorize the caller
299
		SocketStreamWrapper stream(sd);
300
		SspiNegotiateServer server(stream);
301
		ok = server.Authenticate() && server.Authorize();
302
	}
303
	if (!ok) {
304
		closeSocket();
305
		return;
306
	}
307
#endif
308
	// Start output element
309
	established = true; // TODO needs locking?
310
	startOutput();
311
312
	// TODO is locking correct?
313
	// No need to lock in this thread because we don't write to 'sd'
314
	// and 'sd' only gets written to in this thread.
315
	while (true) {
316
		if (sd == OPENMSX_INVALID_SOCKET) return;
317
#ifndef _WIN32
318
		if (poller.poll(sd)) {
319
			break;
320
		}
321
#endif
322
		std::array<char, BUF_SIZE> buf;
323
		auto n = sock_recv(sd, buf.data(), sizeof(buf));
324
		if (n > 0) {
325
			parser.parse(subspan(buf, 0, n));
326
		} else if (n < 0) {
327
			break;
328
		}
329
	}
330
	closeSocket();
331
}
332
333
void SocketConnection::output(std::string_view message_)
334
{
335
	if (!established) { // TODO needs locking?
336
		// Connection isn't authorized yet (and opening tag is not
337
		// yet send). Ignore log and update messages for now.
338
		return;
339
	}
340
	std::span message = message_;
341
	while (!message.empty()) {
342
		ptrdiff_t bytesSend;
343
		{
344
			std::scoped_lock lock(sdMutex);
345
			if (sd == OPENMSX_INVALID_SOCKET) return;
346
			bytesSend = sock_send(sd, message.data(), message.size());
347
		}
348
		if (bytesSend > 0) {
349
			message = message.subspan(bytesSend);
350
		} else {
351
			// Note: On Windows we rely on closing the socket to
352
			//       wake up the worker thread, on other platforms
353
			//       we rely on Poller.
354
			closeSocket();
355
			poller.abort();
356
			break;
357
		}
358
	}
359
}
360
361
void SocketConnection::closeSocket()
362
{
363
	std::scoped_lock lock(sdMutex);
364
	if (sd != OPENMSX_INVALID_SOCKET) {
365
		SOCKET _sd = sd;
366
		sd = OPENMSX_INVALID_SOCKET;
367
		sock_close(_sd);
368
	}
369
}
370
371
void SocketConnection::close()
372
{
373
	closeSocket();
374
}
375
376
bool SocketConnection::isClosed()
377
{
378
	std::scoped_lock lock(sdMutex);
379
	return sd == OPENMSX_INVALID_SOCKET;
380
}
381
382
} // namespace openmsx