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