選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

179 行
5.2 KiB

  1. from __future__ import annotations
  2. import argparse
  3. import asyncio
  4. import os
  5. import sys
  6. from typing import Generator
  7. from .asyncio.client import ClientConnection, connect
  8. from .asyncio.messages import SimpleQueue
  9. from .exceptions import ConnectionClosed
  10. from .frames import Close
  11. from .streams import StreamReader
  12. from .version import version as websockets_version
  13. __all__ = ["main"]
  14. def print_during_input(string: str) -> None:
  15. sys.stdout.write(
  16. # Save cursor position
  17. "\N{ESC}7"
  18. # Add a new line
  19. "\N{LINE FEED}"
  20. # Move cursor up
  21. "\N{ESC}[A"
  22. # Insert blank line, scroll last line down
  23. "\N{ESC}[L"
  24. # Print string in the inserted blank line
  25. f"{string}\N{LINE FEED}"
  26. # Restore cursor position
  27. "\N{ESC}8"
  28. # Move cursor down
  29. "\N{ESC}[B"
  30. )
  31. sys.stdout.flush()
  32. def print_over_input(string: str) -> None:
  33. sys.stdout.write(
  34. # Move cursor to beginning of line
  35. "\N{CARRIAGE RETURN}"
  36. # Delete current line
  37. "\N{ESC}[K"
  38. # Print string
  39. f"{string}\N{LINE FEED}"
  40. )
  41. sys.stdout.flush()
  42. class ReadLines(asyncio.Protocol):
  43. def __init__(self) -> None:
  44. self.reader = StreamReader()
  45. self.messages: SimpleQueue[str] = SimpleQueue()
  46. def parse(self) -> Generator[None, None, None]:
  47. while True:
  48. sys.stdout.write("> ")
  49. sys.stdout.flush()
  50. line = yield from self.reader.read_line(sys.maxsize)
  51. self.messages.put(line.decode().rstrip("\r\n"))
  52. def connection_made(self, transport: asyncio.BaseTransport) -> None:
  53. self.parser = self.parse()
  54. next(self.parser)
  55. def data_received(self, data: bytes) -> None:
  56. self.reader.feed_data(data)
  57. next(self.parser)
  58. def eof_received(self) -> None:
  59. self.reader.feed_eof()
  60. # next(self.parser) isn't useful and would raise EOFError.
  61. def connection_lost(self, exc: Exception | None) -> None:
  62. self.reader.discard()
  63. self.messages.abort()
  64. async def print_incoming_messages(websocket: ClientConnection) -> None:
  65. async for message in websocket:
  66. if isinstance(message, str):
  67. print_during_input("< " + message)
  68. else:
  69. print_during_input("< (binary) " + message.hex())
  70. async def send_outgoing_messages(
  71. websocket: ClientConnection,
  72. messages: SimpleQueue[str],
  73. ) -> None:
  74. while True:
  75. try:
  76. message = await messages.get()
  77. except EOFError:
  78. break
  79. try:
  80. await websocket.send(message)
  81. except ConnectionClosed: # pragma: no cover
  82. break
  83. async def interactive_client(uri: str) -> None:
  84. try:
  85. websocket = await connect(uri)
  86. except Exception as exc:
  87. print(f"Failed to connect to {uri}: {exc}.")
  88. sys.exit(1)
  89. else:
  90. print(f"Connected to {uri}.")
  91. loop = asyncio.get_running_loop()
  92. transport, protocol = await loop.connect_read_pipe(ReadLines, sys.stdin)
  93. incoming = asyncio.create_task(
  94. print_incoming_messages(websocket),
  95. )
  96. outgoing = asyncio.create_task(
  97. send_outgoing_messages(websocket, protocol.messages),
  98. )
  99. try:
  100. await asyncio.wait(
  101. [incoming, outgoing],
  102. # Clean up and exit when the server closes the connection
  103. # or the user enters EOT (^D), whichever happens first.
  104. return_when=asyncio.FIRST_COMPLETED,
  105. )
  106. # asyncio.run() cancels the main task when the user triggers SIGINT (^C).
  107. # https://docs.python.org/3/library/asyncio-runner.html#handling-keyboard-interruption
  108. # Clean up and exit without re-raising CancelledError to prevent Python
  109. # from raising KeyboardInterrupt and displaying a stack track.
  110. except asyncio.CancelledError: # pragma: no cover
  111. pass
  112. finally:
  113. incoming.cancel()
  114. outgoing.cancel()
  115. transport.close()
  116. await websocket.close()
  117. assert websocket.close_code is not None and websocket.close_reason is not None
  118. close_status = Close(websocket.close_code, websocket.close_reason)
  119. print_over_input(f"Connection closed: {close_status}.")
  120. def main(argv: list[str] | None = None) -> None:
  121. parser = argparse.ArgumentParser(
  122. prog="websockets",
  123. description="Interactive WebSocket client.",
  124. add_help=False,
  125. )
  126. group = parser.add_mutually_exclusive_group()
  127. group.add_argument("--version", action="store_true")
  128. group.add_argument("uri", metavar="<uri>", nargs="?")
  129. args = parser.parse_args(argv)
  130. if args.version:
  131. print(f"websockets {websockets_version}")
  132. return
  133. if args.uri is None:
  134. parser.print_usage()
  135. sys.exit(2)
  136. # Enable VT100 to support ANSI escape codes in Command Prompt on Windows.
  137. # See https://github.com/python/cpython/issues/74261 for why this works.
  138. if sys.platform == "win32":
  139. os.system("")
  140. try:
  141. import readline # noqa: F401
  142. except ImportError: # readline isn't available on all platforms
  143. pass
  144. # Remove the try/except block when dropping Python < 3.11.
  145. try:
  146. asyncio.run(interactive_client(args.uri))
  147. except KeyboardInterrupt: # pragma: no cover
  148. pass