communication.py
1 # -*- coding: utf-8 -*-
2 
3 # ***************************************************************************
4 # * *
5 # * Animate workbench - FreeCAD Workbench for lightweight animation *
6 # * Copyright (c) 2019 Jiří Valášek jirka362@gmail.com *
7 # * *
8 # * This file is part of the Animate workbench. *
9 # * *
10 # * This program is free software; you can redistribute it and/or modify *
11 # * it under the terms of the GNU Lesser General Public License (LGPL) *
12 # * as published by the Free Software Foundation; either version 2 of *
13 # * the License, or (at your option) any later version. *
14 # * for detail see the LICENCE text file. *
15 # * *
16 # * Animate workbench is distributed in the hope that it will be useful, *
17 # * but WITHOUT ANY WARRANTY; without even the implied warranty of *
18 # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
19 # * GNU Lesser General Public License for more details. *
20 # * *
21 # * You should have received a copy of the GNU Library General Public *
22 # * License along with Animate workbench; if not, write to the Free *
23 # * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, *
24 # * MA 02111-1307 USA *
25 # * *
26 # ***************************************************************************/
27 
28 
33 
34 import sys
35 try:
36  import FreeCAD
37 except ImportError:
38  pass
39 
40 from PySide2.QtCore import QThread, QByteArray, QDataStream, QIODevice
41 from PySide2.QtNetwork import QTcpServer, QTcpSocket, QAbstractSocket, \
42  QHostAddress
43 
44 
46 SIZEOF_UINT16 = 2
47 
48 
50 SERVER_ERROR_INVALID_ADDRESS = 1
51 
52 
54 SERVER_ERROR_PORT_OCCUPIED = 2
55 
56 
57 WAIT_TIME_MS = 30000
58 
59 
61 COMMAND_EXECUTED_CONFIRMATION_MESSAGE = "Command executed successfully"
62 
63 
65 CLIENT_COMMAND_EXECUTED = 0
66 
67 
69 CLIENT_COMMAND_FAILED = 1
70 
71 
73 CLIENT_ERROR_RESPONSE_NOT_COMPLETE = 2
74 
75 
77 CLIENT_ERROR_NO_RESPONSE = 3
78 
79 
81 CLIENT_ERROR_BLOCK_NOT_WRITTEN = 4
82 
83 
85 CLIENT_ERROR_NO_CONNECTION = 5
86 
87 
88 
96 
97 class CommandThread(QThread):
98 
99 
101 
102 
104 
105 
114 
115  def __init__(self, socketDescriptor, parent):
116  super(CommandThread, self).__init__(parent)
117  self.socketDescriptor = socketDescriptor
118  self.blockSize = 0
119 
120 
131 
132  def run(self):
133  # Try to connect to an incoming tcp socket using its socket descriptor
134  tcpSocket = QTcpSocket()
135  if not tcpSocket.setSocketDescriptor(self.socketDescriptor):
136  FreeCAD.Console.PrintError("Socket not accepted.\n")
137  return
138  FreeCAD.Console.PrintLog("Socket accepted.\n")
139 
140  # Wait for an incoming message
141  if not tcpSocket.waitForReadyRead(msecs=WAIT_TIME_MS):
142  FreeCAD.Console.PrintError("No request send.\n")
143  return
144 
145  # Make an input data stream
146  instr = QDataStream(tcpSocket)
147  instr.setVersion(QDataStream.Qt_4_0)
148 
149  # Try to read the message size
150  if self.blockSize == 0:
151  if tcpSocket.bytesAvailable() < 2:
152  FreeCAD.Console.PrintError("Received message "
153  + "has too few bytes.\n")
154  return
155  self.blockSize = instr.readUInt16()
156 
157  # Check message is sent complete
158  if tcpSocket.bytesAvailable() < self.blockSize:
159  FreeCAD.Console.PrintError("Received message has less bytes "
160  + "then it's supposed to.\n")
161  return
162 
163  # Read message and inform about it
164  cmd = instr.readRawData(self.blockSize).decode("UTF-8")
165  FreeCAD.Console.PrintLog("CommandServer received> "
166  + cmd + "\n")
167 
168  # Try to execute the message string and prepare a response
169  try:
170  exec(cmd)
171  except Exception as e:
172  FreeCAD.Console.PrintError("Executing external command failed:"
173  + str(e) + "\n")
174  message = "Command failed - " + str(e)
175  else:
176  FreeCAD.Console.PrintLog("Executing external command succeeded!\n")
177  message = COMMAND_EXECUTED_CONFIRMATION_MESSAGE
178 
179  # Prepare the data block to send back and inform about it
180  FreeCAD.Console.PrintLog("CommandServer sending> " + message + " \n")
181  block = QByteArray(
182  len(message.encode("UTF-8")).to_bytes(2, byteorder='big')
183  + message.encode("UTF-8"))
184  outstr = QDataStream(block, QIODevice.WriteOnly)
185  outstr.setVersion(QDataStream.Qt_4_0)
186 
187  # Send the block, disconnect from the socket and terminate the QThread
188  tcpSocket.write(block)
189  tcpSocket.disconnectFromHost()
190  tcpSocket.waitForDisconnected()
191 
192 
193 
198 
199 class CommandServer(QTcpServer):
200 
207 
208  def __init__(self, parent=None):
209  super(CommandServer, self).__init__(parent)
210 
211 
221 
222  def incomingConnection(self, socketDescriptor):
223  thread = CommandThread(socketDescriptor, self)
224  thread.finished.connect(thread.deleteLater)
225  thread.start()
226 
227 
229 
230  def close(self):
231  super(CommandServer, self).close()
232  FreeCAD.Console.PrintLog("Server closed.\n")
233 
234 
235 
243 
245  if ip.upper() == "LOCALHOST":
246  return True
247 
248  numbers = ip.split(".")
249  if len(numbers) == 4:
250  if all([(0 <= int(num) <= 255) for num in numbers]):
251  return True
252 
253  return False
254 
255 
256 
272 
273 def startServer(addr, port):
274 
275  if not checkIPIsValid(addr):
276  return SERVER_ERROR_INVALID_ADDRESS
277 
278  if addr.upper() == "LOCALHOST":
279  addr = QHostAddress(QHostAddress.LocalHost)
280  else:
281  addr = QHostAddress(addr)
282 
283  server = CommandServer()
284  if not server.listen(addr, port):
285  FreeCAD.Console.PrintLog("Unable to start the server: %s.\n"
286  % server.errorString())
287  return SERVER_ERROR_PORT_OCCUPIED
288 
289  else:
290  FreeCAD.Console.PrintLog("The server is running on address %s"
291  % server.serverAddress().toString()
292  + " and port %d.\n" % server.serverPort())
293  return server
294 
295 
296 
310 
312 
313 
315 
316 
318 
319 
321 
322 
324 
325 
333 
334  def __init__(self, host, port):
335  self.host = host
336  self.port = port
337  self.tcpSocket = QTcpSocket()
338  self.blockSize = 0
339 
340 
360 
361  def sendCommand(self, cmd):
362 
363  # connect a Qt slot to receive and print errors
364  self.tcpSocket.error.connect(self.displayError)
365 
366  # Try to connect to a host server
367  self.tcpSocket.connectToHost(self.host, self.port, QIODevice.ReadWrite)
368  if not self.tcpSocket.waitForConnected(msecs=WAIT_TIME_MS):
369  if "FreeCAD" in sys.modules:
370  FreeCAD.Console.PrintError("CommandClient.sendCommand error: "
371  + "No connection\n")
372  else:
373  print("CommandClient.sendCommand error: No connection\n")
374  return CLIENT_ERROR_NO_CONNECTION
375 
376  # Prepare a command message to be sent
377  block = QByteArray(
378  len(cmd.encode("UTF-8")).to_bytes(2, byteorder='big')
379  + cmd.encode("UTF-8"))
380  outstr = QDataStream(block, QIODevice.WriteOnly)
381  outstr.setVersion(QDataStream.Qt_4_0)
382 
383  # Try to send the message
384  if "FreeCAD" in sys.modules:
385  FreeCAD.Console.PrintMessage("CommandClient sending> "
386  + cmd + "\n")
387  else:
388  print("CommandClient sending> " + cmd + "\n")
389  self.tcpSocket.write(block)
390  if not self.tcpSocket.waitForBytesWritten(msecs=WAIT_TIME_MS):
391  if "FreeCAD" in sys.modules:
392  FreeCAD.Console.PrintError("CommandClient.sendCommand error: "
393  + "Block not written\n")
394  else:
395  print("CommandClient.sendCommand error: Block not written\n")
396  return CLIENT_ERROR_BLOCK_NOT_WRITTEN
397 
398  # Wait for a response from the host server
399  if not self.tcpSocket.waitForReadyRead(msecs=WAIT_TIME_MS):
400  if "FreeCAD" in sys.modules:
401  FreeCAD.Console.PrintError("CommandClient.sendCommand error: "
402  + "No response received.\n")
403  else:
404  print("CommandClient.sendCommand error: "
405  + "No response received.\n")
406  return CLIENT_ERROR_NO_RESPONSE
407 
408  # Try to read the response
409  instr = QDataStream(self.tcpSocket)
410  instr.setVersion(QDataStream.Qt_4_0)
411  if self.blockSize == 0:
412  if self.tcpSocket.bytesAvailable() < 2:
413  return CLIENT_ERROR_RESPONSE_NOT_COMPLETE
414  self.blockSize = instr.readUInt16()
415 
416  if self.tcpSocket.bytesAvailable() < self.blockSize:
417  return CLIENT_ERROR_RESPONSE_NOT_COMPLETE
418  response = instr.readRawData(self.blockSize).decode("UTF-8")
419  if "FreeCAD" in sys.modules:
420  FreeCAD.Console.PrintMessage("CommandClient received> "
421  + response + "\n")
422  else:
423  print("CommandClient received> " + response + "\n")
424 
425  # Wait until the host server terminates the connection
426  self.tcpSocket.waitForDisconnected()
427  # Reset blockSize to prepare for sending next command
428  self.blockSize = 0
429 
430  # Return value representing a command execution status
431  if response == COMMAND_EXECUTED_CONFIRMATION_MESSAGE:
432  return CLIENT_COMMAND_EXECUTED
433  else:
434  return CLIENT_COMMAND_FAILED
435 
436 
445 
446  def displayError(self, socketError):
447  if socketError != QAbstractSocket.RemoteHostClosedError:
448  if "FreeCAD" in sys.modules:
449  FreeCAD.Console.PrintError("CommandClient error occurred> %s."
450  % self.tcpSocket.errorString()
451  + "\n")
452  else:
453  print("CommandClient error occurred> %s."
454  % self.tcpSocket.errorString() + "\n")
455 
456 
457 
482 
483 def sendClientCommand(host, port, cmd, wait_time=WAIT_TIME_MS):
484  # Try to connect to a host server
485  tcpSocket = QTcpSocket()
486  tcpSocket.connectToHost(host, port, QIODevice.ReadWrite)
487  if not tcpSocket.waitForConnected(msecs=wait_time):
488  return CLIENT_ERROR_NO_CONNECTION
489 
490  # Prepare a command message to be sent
491  block = QByteArray(
492  len(cmd.encode("UTF-8")).to_bytes(2, byteorder='big')
493  + cmd.encode("UTF-8"))
494  outstr = QDataStream(block, QIODevice.WriteOnly)
495  outstr.setVersion(QDataStream.Qt_4_0)
496  tcpSocket.write(block)
497 
498  # Try to send the message
499  if not tcpSocket.waitForBytesWritten(msecs=wait_time):
500  return CLIENT_ERROR_BLOCK_NOT_WRITTEN
501 
502  # Wait for a response from the host server
503  if not tcpSocket.waitForReadyRead(msecs=wait_time):
504  return CLIENT_ERROR_NO_RESPONSE
505 
506  # Try to read the response
507  instr = QDataStream(tcpSocket)
508  instr.setVersion(QDataStream.Qt_4_0)
509  blockSize = 0
510  if blockSize == 0:
511  if tcpSocket.bytesAvailable() < 2:
512  return CLIENT_ERROR_RESPONSE_NOT_COMPLETE
513  blockSize = instr.readUInt16()
514  if tcpSocket.bytesAvailable() < blockSize:
515  return CLIENT_ERROR_RESPONSE_NOT_COMPLETE
516 
517  # Wait until the host server terminates the connection
518  tcpSocket.waitForDisconnected()
519 
520  # Return value representing a command execution status
521  if instr.readRawData(blockSize).decode("UTF-8") \
522  == COMMAND_EXECUTED_CONFIRMATION_MESSAGE:
523  return CLIENT_COMMAND_EXECUTED
524  else:
525  return CLIENT_COMMAND_FAILED
def sendClientCommand(host, port, cmd, wait_time=WAIT_TIME_MS)
Method to be used for sending commands.
QTcpServer class used to receive commands and execute them.
host
A QtHostAddress to the CommandServer.
def startServer(addr, port)
Method used to try to start a CommandServer at a valid IP address and port.
def __init__(self, host, port)
Initialization method for CommandClient.
def run(self)
Thread's functionality method.
def displayError(self, socketError)
Qt's slot method to print out received tcpSocket's error.
Class to be used for sending commands.
port
An int of port at which CommandServer is listening.
blockSize
An int representing size of incoming tcp message.
def close(self)
Method used to close the CommandServer and inform user about it.
def __init__(self, socketDescriptor, parent)
Initialization method for CommandThread.
blockSize
An int representing size of incoming tcp message.
def __init__(self, parent=None)
Initialization method for CommandServer.
QThread class used to receive commands, try to execute and respond to them.
def checkIPIsValid(ip)
Method used to check a selected IP is possible to use with a Qt's QHostAddress.
tcpSocket
A QTcpSocket used to contact CommandSErver
socketDescriptor
A Qt's qintptr socket descriptor to initialize tcpSocket.
def incomingConnection(self, socketDescriptor)
Method to handle an incoming connection by dispatching a CommandThread.
def sendCommand(self, cmd)
Method used to send commands from client to CommandServer.