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