github.com/google/fleetspeak@v0.1.15-0.20240426164851-4f31f62c1aea/fleetspeak_python/fleetspeak/client_connector/connector.py (about) 1 # Copyright 2017 Google Inc. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # https://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 """A client library for fleetspeak daemonservices. 15 16 This library is for use by a process run by the Fleetspeak daemonservice module 17 to send and receive messages. The low level protocol is described in 18 daemonservice/channel/channel.go. 19 """ 20 21 import io 22 import os 23 import platform 24 import struct 25 import threading 26 27 from fleetspeak.src.client.channel.proto.fleetspeak_channel import channel_pb2 28 from fleetspeak.src.common.proto.fleetspeak import common_pb2 29 30 _WINDOWS = platform.system() == "Windows" 31 if _WINDOWS: 32 import msvcrt # pylint: disable=g-import-not-at-top 33 34 35 class ProtocolError(Exception): 36 """Raised when we do not understand the data received from Fleetspeak.""" 37 38 39 # Constants to match behavior of channel.go. 40 _MAGIC = 0xF1EE1001 41 42 # We recommend that messages be ~1MB or smaller, and daemonservice has has 2MB 43 # hardcoded maximum. 44 MAX_SIZE = 2 * 1024 * 1024 45 46 # Format for the struct module to pack/unpack a 32 bit unsigned integer to/from 47 # a little endian byte sequence. 48 _STRUCT_FMT = "<I" 49 50 # The number of bytes required/produced when using _STRUCT_FMT. 51 _STRUCT_LEN = 4 52 53 # Environment variables, used to find the filedescriptors left open for 54 # us when started by Fleetspeak. 55 _INFD_VAR = "FLEETSPEAK_COMMS_CHANNEL_INFD" 56 _OUTFD_VAR = "FLEETSPEAK_COMMS_CHANNEL_OUTFD" 57 58 59 def _EnvOpen(var, mode): 60 """Open a file descriptor identified by an environment variable.""" 61 value = os.getenv(var) 62 if value is None: 63 raise ValueError("%s is not set" % var) 64 65 fd = int(value) 66 67 # If running on Windows, convert the file handle to a C file descriptor; see: 68 # https://groups.google.com/forum/#!topic/dev-python/GeN5bFJWfJ4 69 if _WINDOWS: 70 fd = msvcrt.open_osfhandle(fd, 0) 71 72 return io.open(fd, mode) 73 74 75 class FleetspeakConnection(object): 76 """A connection to the Fleetspeak system. 77 78 It's safe to call methods of this class in parallel. 79 """ 80 81 def __init__(self, version=None, read_file=None, write_file=None): 82 """Connect to Fleetspeak. 83 84 Connects to and begins an initial exchange of magic numbers with the 85 Fleetspeak process. In normal use, the arguments are not required and will 86 be created using the environment variables set by daemonservice. 87 88 Args: 89 version: A string identifying the version of the service being run. Will 90 be included in resource reports for this service. 91 read_file: A python file object, or similar, used to read bytes from 92 Fleetspeak. If None, will be created based on the execution environment 93 provided by daemonservice. 94 write_file: A python file object, or similar, used to write bytes to 95 Fleetspeak. If None, will be created based on the execution environment 96 provided by daemonservice. 97 98 Raises: 99 ValueError: If read_file and write_file are not provided, and the 100 corresponding environment variables are not set. 101 ProtocolError: If we receive unexpected data from Fleetspeak. 102 """ 103 self._read_file = read_file 104 if not self._read_file: 105 self._read_file = _EnvOpen(_INFD_VAR, "rb") 106 107 self._read_lock = threading.Lock() 108 109 self._write_file = write_file 110 if not self._write_file: 111 self._write_file = _EnvOpen(_OUTFD_VAR, "wb") 112 113 self._write_lock = threading.Lock() 114 115 # It is safer to send the magic number before reading it, in case the other 116 # end does the same. Also, we'll be killed as unresponsive if we don't 117 # write the magic number quickly enough. (Currently though, the other end is 118 # the go implementation, which reads and writes in parallel.) 119 self._WriteMagic() 120 121 self._WriteStartupData(version) 122 self._ReadMagic() 123 124 def Send(self, message): 125 """Send a message through Fleetspeak. 126 127 Args: 128 message: A message protocol buffer. 129 130 Returns: 131 Size of the message in bytes. 132 Raises: 133 ValueError: If message is not a common_pb2.Message. 134 """ 135 if not isinstance(message, common_pb2.Message): 136 raise ValueError("Send requires a fleetspeak.Message") 137 138 if message.destination.service_name == "system": 139 raise ValueError( 140 "Only predefined messages can have destination.service_name ==" 141 ' "system"' 142 ) 143 144 return self._SendImpl(message) 145 146 def _SendImpl(self, message): 147 if not isinstance(message, common_pb2.Message): 148 raise ValueError("Send requires a fleetspeak.Message") 149 150 buf = message.SerializeToString() 151 if len(buf) > MAX_SIZE: 152 raise ValueError( 153 "Serialized message too large, size must be at most %d, got %d" 154 % (MAX_SIZE, len(buf)) 155 ) 156 157 with self._write_lock: 158 self._write_file.write(struct.pack(_STRUCT_FMT, len(buf))) 159 self._write_file.write(buf) 160 self._WriteMagic() 161 162 return len(buf) 163 164 def Recv(self): 165 """Accept a message from Fleetspeak. 166 167 Returns: 168 A tuple (common_pb2.Message, size of the message in bytes). 169 Raises: 170 ProtocolError: If we receive unexpected data from Fleetspeak. 171 """ 172 size = struct.unpack(_STRUCT_FMT, self._ReadN(_STRUCT_LEN))[0] 173 if size > MAX_SIZE: 174 raise ProtocolError( 175 "Expected size to be at most %d, got %d" % (MAX_SIZE, size) 176 ) 177 with self._read_lock: 178 buf = self._ReadN(size) 179 self._ReadMagic() 180 181 res = common_pb2.Message() 182 res.ParseFromString(buf) 183 184 return res, len(buf) 185 186 def Heartbeat(self): 187 """Sends a heartbeat to the Fleetspeak client. 188 189 If this daemonservice is configured to use heartbeats, clients that don't 190 call this method often enough are considered faulty and are restarted by 191 Fleetspeak. 192 """ 193 heartbeat_msg = common_pb2.Message( 194 message_type="Heartbeat", 195 destination=common_pb2.Address(service_name="system"), 196 ) 197 self._SendImpl(heartbeat_msg) 198 199 def _ReadMagic(self): 200 got = struct.unpack(_STRUCT_FMT, self._ReadN(_STRUCT_LEN))[0] 201 if got != _MAGIC: 202 raise ProtocolError( 203 "Expected to read magic number {}, got {}.".format(_MAGIC, got) 204 ) 205 206 def _WriteMagic(self): 207 buf = struct.pack(_STRUCT_FMT, _MAGIC) 208 self._write_file.write(buf) 209 self._write_file.flush() 210 211 def _WriteStartupData(self, version): 212 startup_msg = common_pb2.Message( 213 message_type="StartupData", 214 destination=common_pb2.Address(service_name="system"), 215 ) 216 startup_msg.data.Pack( 217 channel_pb2.StartupData(pid=os.getpid(), version=version) 218 ) 219 self._SendImpl(startup_msg) 220 221 def _ReadN(self, n): 222 """Reads n characters from the input stream, or until EOF. 223 224 This is equivalent to the current CPython implementation of read(n), but 225 not guaranteed by the docs. 226 227 Args: 228 n: int 229 230 Returns: 231 string 232 """ 233 ret = b"" 234 while True: 235 chunk = self._read_file.read(n - len(ret)) 236 ret += chunk 237 238 if len(ret) == n or not chunk: 239 return ret