Skip to content

Commit 2ddb0ce

Browse files
committed
Gracefully shutdown and restart tornado server when plugin is run
1 parent dd207e9 commit 2ddb0ce

1 file changed

Lines changed: 91 additions & 51 deletions

File tree

ida/idacode_utils/plugin.py

Lines changed: 91 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,93 @@
1-
import socket, sys, os, threading, inspect, subprocess
1+
import sys, threading, subprocess, logging
22
import idacode_utils.settings as settings
33
try:
44
import tornado, debugpy
55
except ImportError:
66
print("[IDACode] Dependencies missing, run:\n \"{}\" -m pip install --user debugpy tornado".format(settings.PYTHON))
77
sys.exit()
88
import idaapi
9-
import idacode_utils.dbg as dbg
10-
import idacode_utils.hooks as hooks
119
from idacode_utils.socket_handler import SocketHandler
1210

11+
# Source: https://github.com/OALabs/hexcopy-ida/blob/8b0b2a3021d7dc9010c01821b65a80c47d491b61/hexcopy.py#L30
12+
major, minor = map(int, idaapi.get_kernel_version().split("."))
13+
using_ida7api = (major > 6)
14+
using_pyqt5 = using_ida7api or (major == 6 and minor >= 9)
15+
16+
if using_pyqt5:
17+
import PyQt5.QtWidgets as QtWidgets
18+
else:
19+
import PySide.QtGui as QtGui
20+
QtWidgets = QtGui
21+
1322
# Fix for https://github.com/tornadoweb/tornado/issues/2608
1423
if sys.version_info >= (3, 4):
1524
import asyncio
1625

17-
VERSION = "0.3.0"
18-
initialized = False
26+
def join_gui_thread(thread: threading.Thread, timeout=None):
27+
iterations = 0
28+
iteration_timeout = 0.1
29+
while True:
30+
if not thread.is_alive():
31+
return True
32+
thread.join(iteration_timeout)
33+
QtWidgets.QApplication.processEvents()
34+
if timeout is not None and iteration_timeout * iterations >= timeout:
35+
return False
36+
iterations += 1
37+
38+
class Server:
39+
def __init__(self):
40+
self.started = False
41+
self.server: tornado.httpserver.HTTPServer = None
42+
self.thread: threading.Thread = None
43+
44+
def start(self):
45+
self.stop()
46+
self.thread = threading.Thread(target=self.server_thread)
47+
self.thread.start()
48+
self.started = True
1949

20-
def setup_patches():
21-
hooks.install()
22-
#sys.executable = settings.PYTHON
50+
def stop(self):
51+
if not self.started:
52+
return
2353

24-
def create_socket_handler():
25-
if sys.version_info >= (3, 4):
26-
asyncio.set_event_loop(asyncio.new_event_loop())
27-
app = tornado.web.Application([
28-
(r"/ws", SocketHandler),
29-
])
30-
server = tornado.httpserver.HTTPServer(app)
31-
print("[IDACode] Listening on {address}:{port}".format(address=settings.HOST, port=settings.PORT))
32-
server.listen(address=settings.HOST, port=settings.PORT)
54+
if self.server is not None:
55+
self.io_loop.add_callback(self.server.stop)
56+
self.io_loop.add_callback(self.server.close_all_connections)
57+
self.io_loop.add_callback(self.io_loop.stop)
3358

34-
def start_server():
35-
# Fix for https://github.com/tornadoweb/tornado/issues/2608
36-
if sys.platform=='win32' and sys.version_info >= (3,8):
37-
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
59+
if not join_gui_thread(self.thread, 1.0):
60+
print("[IDACode] Waiting for server to stop...")
61+
if not join_gui_thread(self.thread, 5.0):
62+
print("[IDACode] deadlock while stopping server, please report an issue!\n")
63+
self.thread = None
64+
self.server = None
65+
print("[IDACode] Server stopped")
3866

39-
setup_patches()
40-
create_socket_handler()
41-
tornado.ioloop.IOLoop.current().start()
67+
def server_thread(self):
68+
# Create a new event loop for the thread
69+
# https://github.com/tornadoweb/tornado/issues/2308#issuecomment-372582005
70+
loop = asyncio.new_event_loop()
71+
loop.set_debug(False)
72+
logging.getLogger("asyncio").setLevel(logging.CRITICAL) # Remove some debug spam
73+
asyncio.set_event_loop(loop)
74+
75+
# Before starting the event loop, instantiate a WebSocketClient and add a
76+
# callback to the event loop to start it. This way the first thing the
77+
# event loop does is to start the client.
78+
self.io_loop = tornado.ioloop.IOLoop.current()
79+
app = tornado.web.Application([
80+
(r"/ws", SocketHandler),
81+
])
82+
self.server = tornado.httpserver.HTTPServer(app)
83+
print("[IDACode] Listening on {address}:{port}".format(address=settings.HOST, port=settings.PORT))
84+
self.server.listen(address=settings.HOST, port=settings.PORT)
85+
86+
# Start the event loop.
87+
self.io_loop.start()
88+
89+
# Signal that the service is finished
90+
self.started = False
4291

4392
def get_python_versions():
4493
settings_version = subprocess.check_output([settings.PYTHON, "-c", "import sys; print(sys.version + sys.platform)"])
@@ -47,36 +96,27 @@ def get_python_versions():
4796
return (settings_version, ida_version)
4897

4998
class IDACode(idaapi.plugin_t):
50-
def __init__(self):
51-
self.flags = idaapi.PLUGIN_UNL
52-
self.comment = "IDACode"
53-
self.help = "IDACode"
54-
self.wanted_name = "IDACode"
55-
self.wanted_hotkey = ""
99+
flags = idaapi.PLUGIN_KEEP
100+
comment = "IDACode"
101+
help = "IDACode"
102+
wanted_name = "IDACode"
103+
wanted_hotkey = "Ctrl-Shift-I"
56104

57105
def init(self):
58-
global initialized
59-
if not initialized:
60-
initialized = True
61-
if os.path.isfile(settings.PYTHON):
62-
settings_version, ida_version = get_python_versions()
63-
if settings_version != ida_version:
64-
print("[IDACode] settings.PYTHON version mismatch, aborting load:")
65-
print("[IDACode] IDA interpreter: {}".format(ida_version))
66-
print("[IDACode] settings.PYTHON: {}".format(settings_version))
67-
return idaapi.PLUGIN_SKIP
68-
else:
69-
print("[IDACode] settings.PYTHON ({}) does not exist, aborting load".format(settings.PYTHON))
70-
print("[IDACode] To fix this issue, modify idacode_utils/settings.py to point to the python executable")
71-
return idaapi.PLUGIN_SKIP
72-
print("[IDACode] Plugin version {}".format(VERSION))
73-
print("[IDACode] Plugin loaded, use Edit -> Plugins -> IDACode to start the server")
74-
return idaapi.PLUGIN_OK
106+
settings_version, ida_version = get_python_versions()
107+
if settings_version != ida_version:
108+
print("[IDACode] settings.PYTHON version mismatch, aborting load:")
109+
print("[IDACode] IDA interpreter: {}".format(ida_version))
110+
print("[IDACode] settings.PYTHON: {}".format(settings_version))
111+
return idaapi.PLUGIN_SKIP
112+
113+
self.server = Server()
114+
print("[IDACode] Plugin version 0.4.0")
115+
print("[IDACode] Plugin loaded, use Edit -> Plugins -> IDACode to start the server")
116+
return idaapi.PLUGIN_KEEP
75117

76118
def run(self, args):
77-
thread = threading.Thread(target=start_server)
78-
thread.daemon = True
79-
thread.start()
119+
self.server.start()
80120

81121
def term(self):
82-
pass
122+
self.server.stop()

0 commit comments

Comments
 (0)