1- import socket , sys , os , threading , inspect , subprocess
1+ import sys , threading , subprocess , logging
22import idacode_utils .settings as settings
33try :
44 import tornado , debugpy
55except ImportError :
66 print ("[IDACode] Dependencies missing, run:\n \" {}\" -m pip install --user debugpy tornado" .format (settings .PYTHON ))
77 sys .exit ()
88import idaapi
9- import idacode_utils .dbg as dbg
10- import idacode_utils .hooks as hooks
119from 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
1423if 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
4392def 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
4998class 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