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