/web3/providers/ipc.py

https://github.com/ethereum/web3.py · Python · 272 lines · 227 code · 39 blank · 6 comment · 49 complexity · 1c3de8c56d9e49dfa38437623b537011 MD5 · raw file

  1. from json import (
  2. JSONDecodeError,
  3. )
  4. import logging
  5. import os
  6. from pathlib import (
  7. Path,
  8. )
  9. import socket
  10. import sys
  11. import threading
  12. from types import (
  13. TracebackType,
  14. )
  15. from typing import (
  16. Any,
  17. Type,
  18. Union,
  19. )
  20. from web3._utils.threads import (
  21. Timeout,
  22. )
  23. from web3.types import (
  24. RPCEndpoint,
  25. RPCResponse,
  26. )
  27. from .base import (
  28. JSONBaseProvider,
  29. )
  30. def get_ipc_socket(ipc_path: str, timeout: float=0.1) -> socket.socket:
  31. if sys.platform == 'win32':
  32. # On Windows named pipe is used. Simulate socket with it.
  33. from web3._utils.windows import NamedPipe
  34. return NamedPipe(ipc_path)
  35. else:
  36. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  37. sock.connect(ipc_path)
  38. sock.settimeout(timeout)
  39. return sock
  40. class PersistantSocket:
  41. sock = None
  42. def __init__(self, ipc_path: str) -> None:
  43. self.ipc_path = ipc_path
  44. def __enter__(self) -> socket.socket:
  45. if not self.ipc_path:
  46. raise FileNotFoundError("cannot connect to IPC socket at path: %r" % self.ipc_path)
  47. if not self.sock:
  48. self.sock = self._open()
  49. return self.sock
  50. def __exit__(
  51. self, exc_type: Type[BaseException], exc_value: BaseException, traceback: TracebackType
  52. ) -> None:
  53. # only close the socket if there was an error
  54. if exc_value is not None:
  55. try:
  56. self.sock.close()
  57. except Exception:
  58. pass
  59. self.sock = None
  60. def _open(self) -> socket.socket:
  61. return get_ipc_socket(self.ipc_path)
  62. def reset(self) -> socket.socket:
  63. self.sock.close()
  64. self.sock = self._open()
  65. return self.sock
  66. # type ignored b/c missing return statement is by design here
  67. def get_default_ipc_path() -> str: # type: ignore
  68. if sys.platform == 'darwin':
  69. ipc_path = os.path.expanduser(os.path.join(
  70. "~",
  71. "Library",
  72. "Ethereum",
  73. "geth.ipc"
  74. ))
  75. if os.path.exists(ipc_path):
  76. return ipc_path
  77. ipc_path = os.path.expanduser(os.path.join(
  78. "~",
  79. "Library",
  80. "Application Support",
  81. "io.parity.ethereum",
  82. "jsonrpc.ipc"
  83. ))
  84. if os.path.exists(ipc_path):
  85. return ipc_path
  86. base_trinity_path = Path('~').expanduser() / '.local' / 'share' / 'trinity'
  87. ipc_path = str(base_trinity_path / 'mainnet' / 'ipcs-eth1' / 'jsonrpc.ipc')
  88. if Path(ipc_path).exists():
  89. return str(ipc_path)
  90. elif sys.platform.startswith('linux') or sys.platform.startswith('freebsd'):
  91. ipc_path = os.path.expanduser(os.path.join(
  92. "~",
  93. ".ethereum",
  94. "geth.ipc"
  95. ))
  96. if os.path.exists(ipc_path):
  97. return ipc_path
  98. ipc_path = os.path.expanduser(os.path.join(
  99. "~",
  100. ".local",
  101. "share",
  102. "io.parity.ethereum",
  103. "jsonrpc.ipc"
  104. ))
  105. if os.path.exists(ipc_path):
  106. return ipc_path
  107. base_trinity_path = Path('~').expanduser() / '.local' / 'share' / 'trinity'
  108. ipc_path = str(base_trinity_path / 'mainnet' / 'ipcs-eth1' / 'jsonrpc.ipc')
  109. if Path(ipc_path).exists():
  110. return str(ipc_path)
  111. elif sys.platform == 'win32':
  112. ipc_path = os.path.join(
  113. "\\\\",
  114. ".",
  115. "pipe",
  116. "geth.ipc"
  117. )
  118. if os.path.exists(ipc_path):
  119. return ipc_path
  120. ipc_path = os.path.join(
  121. "\\\\",
  122. ".",
  123. "pipe",
  124. "jsonrpc.ipc"
  125. )
  126. if os.path.exists(ipc_path):
  127. return ipc_path
  128. else:
  129. raise ValueError(
  130. "Unsupported platform '{0}'. Only darwin/linux/win32/freebsd are "
  131. "supported. You must specify the ipc_path".format(sys.platform)
  132. )
  133. # type ignored b/c missing return statement is by design here
  134. def get_dev_ipc_path() -> str: # type: ignore
  135. if sys.platform == 'darwin':
  136. tmpdir = os.environ.get('TMPDIR', '')
  137. ipc_path = os.path.expanduser(os.path.join(
  138. tmpdir,
  139. "geth.ipc"
  140. ))
  141. if os.path.exists(ipc_path):
  142. return ipc_path
  143. elif sys.platform.startswith('linux') or sys.platform.startswith('freebsd'):
  144. ipc_path = os.path.expanduser(os.path.join(
  145. "/tmp",
  146. "geth.ipc"
  147. ))
  148. if os.path.exists(ipc_path):
  149. return ipc_path
  150. elif sys.platform == 'win32':
  151. ipc_path = os.path.join(
  152. "\\\\",
  153. ".",
  154. "pipe",
  155. "geth.ipc"
  156. )
  157. if os.path.exists(ipc_path):
  158. return ipc_path
  159. ipc_path = os.path.join(
  160. "\\\\",
  161. ".",
  162. "pipe",
  163. "jsonrpc.ipc"
  164. )
  165. if os.path.exists(ipc_path):
  166. return ipc_path
  167. else:
  168. raise ValueError(
  169. "Unsupported platform '{0}'. Only darwin/linux/win32/freebsd are "
  170. "supported. You must specify the ipc_path".format(sys.platform)
  171. )
  172. class IPCProvider(JSONBaseProvider):
  173. logger = logging.getLogger("web3.providers.IPCProvider")
  174. _socket = None
  175. def __init__(
  176. self,
  177. ipc_path: Union[str, Path] = None,
  178. timeout: int = 10,
  179. *args: Any,
  180. **kwargs: Any,
  181. ) -> None:
  182. if ipc_path is None:
  183. self.ipc_path = get_default_ipc_path()
  184. elif isinstance(ipc_path, str) or isinstance(ipc_path, Path):
  185. self.ipc_path = str(Path(ipc_path).expanduser().resolve())
  186. else:
  187. raise TypeError("ipc_path must be of type string or pathlib.Path")
  188. self.timeout = timeout
  189. self._lock = threading.Lock()
  190. self._socket = PersistantSocket(self.ipc_path)
  191. super().__init__()
  192. def __str__(self) -> str:
  193. return f"<{self.__class__.__name__} {self.ipc_path}>"
  194. def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
  195. self.logger.debug("Making request IPC. Path: %s, Method: %s",
  196. self.ipc_path, method)
  197. request = self.encode_rpc_request(method, params)
  198. with self._lock, self._socket as sock:
  199. try:
  200. sock.sendall(request)
  201. except BrokenPipeError:
  202. # one extra attempt, then give up
  203. sock = self._socket.reset()
  204. sock.sendall(request)
  205. raw_response = b""
  206. with Timeout(self.timeout) as timeout:
  207. while True:
  208. try:
  209. raw_response += sock.recv(4096)
  210. except socket.timeout:
  211. timeout.sleep(0)
  212. continue
  213. if raw_response == b"":
  214. timeout.sleep(0)
  215. elif has_valid_json_rpc_ending(raw_response):
  216. try:
  217. response = self.decode_rpc_response(raw_response)
  218. except JSONDecodeError:
  219. timeout.sleep(0)
  220. continue
  221. else:
  222. return response
  223. else:
  224. timeout.sleep(0)
  225. continue
  226. # A valid JSON RPC response can only end in } or ] http://www.jsonrpc.org/specification
  227. def has_valid_json_rpc_ending(raw_response: bytes) -> bool:
  228. stripped_raw_response = raw_response.rstrip()
  229. for valid_ending in [b"}", b"]"]:
  230. if stripped_raw_response.endswith(valid_ending):
  231. return True
  232. else:
  233. return False