Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6d7db90

Browse files
nirizrshiftre
authored andcommittedFeb 27, 2017
Add match results dialog (#17)
* Add match results dialog * Expose matches as read only data through rest api Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Fix hash match score calc, ignore matches below 50, log # of matches Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Start match results dialog at end of match Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Add task matches designated interface and start using it in MatchResultsDialog Signed-off-by: Nir Izraeli <nirizr@gmail.com> * result dialogs code cleanups Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Remove some commented out old code, rename a function Signed-off-by: Nir Izraeli <nirizr@gmail.com> * minor cleanups and fix results dialog show Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Fix tree sorting and other minor ui issues Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Fix multiple filter script dialog issues and create default script file Signed-off-by: Nir Izraeli <nirizr@gmail.com> * use .pyf instead of py for filter scripts Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Couple renames Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Make result requests delayed and more name changes Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Simplify and cleanup populate_tree plus some minor fixes Signed-off-by: Nir Izraeli <nirizr@gmail.com> * some more tiny cleanups Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Add pagination to match results and basic pagination infrastructure Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Move client side name generation logic to server side server directly uses the name annotation of an object to generate an appropriate name (or a 'sub_{offset}' otherwise). a name attribute is now generated as part of the serialized object, so the client side needs only display that Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Add tests to increase coverage And a minor codacy fix Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Simplify Match model to ease database insertion process which proved to be quite slow * Split match results requests into three: locals, remotes, matches + performance optimizations and some debugging Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Make Modal configurable and default to false in results dialog Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Move result download logic to action, add progress bar Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Remove remote graph display placeholder in favor of planned graph Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Add new serialized graph dialog displaying remote functions Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Add annotation view endpoint Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Show remote function in an additional graph view Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Jump to local code on results selection Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Cancel results download when user cancels Also, small refactor of QueryWorker and surrounding functions Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Replace delayed_query calls with QueryWorker instances Replace delayed_query calls which potentially risked double starting a query worker (and hang). Replace delayed_worker calls with calling query worker's start method Simplify network delayed query interface Signed-off-by: Nir Izraeli <nirizr@gmail.com> * don't query unneeded db field Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Simplify cancel and reject in MatchDialog Signed-off-by: Nir Izraeli <nirizr@gmail.com> * More cleanups by reusing pbar and timer in match action Plus better exception logging Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Fix previusly uncaught minor bugs Signed-off-by: Nir Izraeli <nirizr@gmail.com> * add locals/remotes/matches to MatchResultDialog as they come instead caching them in MatchAction Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Cleanup preform_upload and fix not uploading last functions unless threshold is reached * Avoid exception and remove related exception handling code * Upload serialized instances when all functions were serialized but didn't reach upload threshold Signed-off-by: Nir Izraeli <nirizr@gmail.com> * Fix progressbar accept on match result read Signed-off-by: Nir Izraeli <nirizr@gmail.com> * fix CR comments * more logs and a few additional comments * validate pagination network support Signed-off-by: Nir Izraeli <nirizr@gmail.com>
1 parent e43f6ea commit 6d7db90

19 files changed

+973
-148
lines changed
 

‎idaplugin/rematch/actions/match.py

+135-48
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import idautils
33

44
from ..dialogs.match import MatchDialog
5+
from ..dialogs.matchresult import MatchResultDialog
56

67
from .. import instances
78
from .. import network, netnode, log
@@ -17,8 +18,7 @@ class MatchAction(base.BoundFileAction):
1718
def __init__(self, *args, **kwargs):
1819
super(MatchAction, self).__init__(*args, **kwargs)
1920
self.functions = None
20-
self.pbar = None
21-
self.timer = None
21+
self.results = None
2222
self.task_id = None
2323
self.file_version_id = None
2424
self.instance_set = []
@@ -31,6 +31,37 @@ def __init__(self, *args, **kwargs):
3131
self.target_file = None
3232
self.methods = None
3333

34+
self.delayed_queries = []
35+
36+
self.pbar = QtWidgets.QProgressDialog()
37+
self.pbar.canceled.connect(self.cancel)
38+
self.pbar.rejected.connect(self.cancel)
39+
self.pbar.hide()
40+
41+
self.timer = QtCore.QTimer()
42+
43+
def clean(self):
44+
self.timer.stop()
45+
try:
46+
self.timer.timeout.disconnect()
47+
except TypeError:
48+
pass
49+
try:
50+
self.pbar.accepted.disconnect()
51+
except TypeError:
52+
pass
53+
54+
def cancel_delayed(self):
55+
for delayed in self.delayed_queries:
56+
log('match_action').info("async task cancelled: %s", repr(delayed))
57+
delayed.cancel()
58+
self.delayed_queries = []
59+
60+
def cancel(self):
61+
log('match_action').info("match action cancelled")
62+
self.clean()
63+
self.cancel_delayed()
64+
3465
@staticmethod
3566
def calc_file_version_hash():
3667
version_obj = {}
@@ -69,62 +100,55 @@ def response_handler(self, file_version):
69100
return True
70101

71102
def start_upload(self):
103+
log('match_action').info("Data upload started")
104+
72105
self.functions = set(idautils.Functions())
73106

74-
self.pbar = QtWidgets.QProgressDialog()
75107
self.pbar.setLabelText("Processing IDB... You may continue working,\nbut "
76108
"please avoid making any ground-breaking changes.")
77109
self.pbar.setRange(0, len(self.functions))
78110
self.pbar.setValue(0)
79-
self.pbar.canceled.connect(self.cancel_upload)
80-
self.pbar.rejected.connect(self.reject_upload)
81111
self.pbar.accepted.connect(self.accept_upload)
112+
self.pbar.show()
82113

83-
self.timer = QtCore.QTimer()
84114
self.timer.timeout.connect(self.perform_upload)
85115
self.timer.start(0)
86116

87117
return True
88118

89119
def perform_upload(self):
90-
try:
91-
offset = self.functions.pop()
92-
except KeyError:
93-
self.timer.stop()
120+
if not self.functions:
94121
return
95122

96-
try:
97-
func = instances.FunctionInstance(self.file_version_id, offset)
98-
self.instance_set.append(func.serialize())
99-
100-
if len(self.instance_set) >= 100:
101-
network.delayed_query("POST", "collab/instances/",
102-
params=self.instance_set, json=True,
103-
callback=self.progress_advance)
104-
self.instance_set = []
105-
self.pbar.setMaximum(self.pbar.maximum() + 1)
106-
self.progress_advance()
107-
except Exception:
108-
self.cancel_upload()
109-
raise
123+
# pop a function, serialize and add to the ready set
124+
offset = self.functions.pop()
125+
func = instances.FunctionInstance(self.file_version_id, offset)
126+
self.instance_set.append(func.serialize())
127+
128+
# if ready set contains 100 or more functions, or if we just poped the last
129+
# function clear and upload entire ready set to the server.
130+
if len(self.instance_set) >= 100 or not self.functions:
131+
q = network.QueryWorker("POST", "collab/instances/",
132+
params=self.instance_set, json=True)
133+
q.start(self.progress_advance)
134+
self.instance_set = []
135+
self.pbar.setMaximum(self.pbar.maximum() + 1)
136+
self.progress_advance()
110137

111138
def progress_advance(self, result=None):
112139
del result
113140
new_value = self.pbar.value() + 1
114-
self.pbar.setValue(new_value)
115141
if new_value >= self.pbar.maximum():
116142
self.pbar.accept()
143+
else:
144+
self.pbar.setValue(new_value)
117145

118-
def cancel_upload(self):
119-
self.timer.stop()
120-
self.timer = None
121-
self.pbar = None
146+
def accept_upload(self):
147+
log('match_action').info("Data upload completed successfully")
122148

123-
def reject_upload(self):
124-
self.cancel_upload()
149+
self.clean()
150+
self.delayed_queries = []
125151

126-
def accept_upload(self):
127-
self.cancel_upload()
128152
self.start_task()
129153

130154
def start_task(self):
@@ -148,19 +172,15 @@ def start_task(self):
148172
r = network.query("POST", "collab/tasks/", params=params, json=True)
149173
self.task_id = r['id']
150174

151-
self.pbar = QtWidgets.QProgressDialog()
152175
self.pbar.setLabelText("Waiting for remote matching... You may continue "
153176
"working without any limitations.")
154177
self.pbar.setRange(0, int(r['progress_max']) if r['progress_max'] else 0)
155178
self.pbar.setValue(int(r['progress']))
156-
self.pbar.canceled.connect(self.cancel_task)
157-
self.pbar.rejected.connect(self.reject_task)
158179
self.pbar.accepted.connect(self.accept_task)
159180
self.pbar.show()
160181

161-
self.timer = QtCore.QTimer()
162182
self.timer.timeout.connect(self.perform_task)
163-
self.timer.start(1000)
183+
self.timer.start(200)
164184

165185
def perform_task(self):
166186
try:
@@ -171,24 +191,91 @@ def perform_task(self):
171191
progress = int(r['progress'])
172192
status = r['status']
173193
if status == 'failed':
174-
self.pbar.reject()
194+
self.pbar.cancel()
175195
elif progress_max:
176196
self.pbar.setMaximum(progress_max)
177197
if progress >= progress_max:
178198
self.pbar.accept()
179199
else:
180200
self.pbar.setValue(progress)
181201
except Exception:
182-
self.cancel_task()
202+
self.cancel()
203+
log('match_action').exception("perform update failed")
183204
raise
184205

185-
def cancel_task(self):
186-
self.timer.stop()
187-
self.timer = None
188-
self.pbar = None
206+
def accept_task(self):
207+
log('match_action').info("Remote task completed successfully")
189208

190-
def reject_task(self):
191-
self.cancel_task()
209+
self.clean()
210+
self.delayed_queries = []
192211

193-
def accept_task(self):
194-
self.cancel_task()
212+
self.start_results()
213+
214+
def start_results(self):
215+
self.pbar.setLabelText("Receiving match results...")
216+
self.pbar.setRange(0, 0)
217+
self.pbar.setValue(0)
218+
self.pbar.accepted.connect(self.accept_results)
219+
self.pbar.show()
220+
221+
self.results = MatchResultDialog(self.task_id)
222+
223+
log('match_action').info("Result download started")
224+
locals_url = "collab/tasks/{}/locals/".format(self.task_id)
225+
q = network.QueryWorker("GET", locals_url, json=True, paginate=True,
226+
params={'limit': 100})
227+
q.start(self.handle_locals)
228+
self.delayed_queries.append(q)
229+
230+
remotes_url = "collab/tasks/{}/remotes/".format(self.task_id)
231+
q = network.QueryWorker("GET", remotes_url, json=True, paginate=True,
232+
params={'limit': 100})
233+
q.start(self.handle_remotes)
234+
self.delayed_queries.append(q)
235+
236+
matches_url = "collab/tasks/{}/matches/".format(self.task_id)
237+
q = network.QueryWorker("GET", matches_url, json=True, paginate=True,
238+
params={'limit': 100})
239+
q.start(self.handle_matches)
240+
self.delayed_queries.append(q)
241+
242+
def handle_locals(self, response):
243+
new_locals = {obj['id']: obj for obj in response['results']}
244+
self.results.add_locals(new_locals)
245+
246+
self.handle_page(response)
247+
248+
def handle_remotes(self, response):
249+
new_remotes = {obj['id']: obj for obj in response['results']}
250+
self.results.add_remotes(new_remotes)
251+
252+
self.handle_page(response)
253+
254+
def handle_matches(self, response):
255+
def rename(o):
256+
o['local_id'] = o.pop('from_instance')
257+
o['remote_id'] = o.pop('to_instance')
258+
return o
259+
260+
new_matches = map(rename, response['results'])
261+
self.results.add_matches(new_matches)
262+
263+
self.handle_page(response)
264+
265+
def handle_page(self, response):
266+
if 'previous' not in response or not response['previous']:
267+
self.pbar.setMaximum(self.pbar.maximum() + response['count'])
268+
269+
new_value = max(self.pbar.value(), 0) + len(response['results'])
270+
if new_value >= self.pbar.maximum():
271+
self.pbar.accept()
272+
else:
273+
self.pbar.setValue(new_value)
274+
275+
def accept_results(self):
276+
log('match_action').info("Result download completed successfully")
277+
278+
self.clean()
279+
self.delayed_queries = []
280+
281+
self.results.show()

‎idaplugin/rematch/dialogs/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
import match
44
import project
55
import settings
6+
import serializedgraph
67

7-
__all__ = [base, login, match, project, settings]
8+
__all__ = ['base', 'login', 'match', 'project', 'settings', 'serializedgraph']

‎idaplugin/rematch/dialogs/base.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88

99
class BaseDialog(QtWidgets.QDialog):
10-
def __init__(self, title="", reject_handler=None, submit_handler=None,
11-
response_handler=None, exception_handler=None, **kwargs):
10+
def __init__(self, title="", modal=True, reject_handler=None,
11+
submit_handler=None, response_handler=None,
12+
exception_handler=None, **kwargs):
1213
super(BaseDialog, self).__init__(**kwargs)
13-
self.setModal(True)
14+
self.setModal(modal)
1415
self.setWindowTitle(title)
1516
self.reject_handler = reject_handler
1617
self.submit_handler = submit_handler
@@ -61,8 +62,7 @@ def submit_base(self):
6162
return
6263

6364
# if received a query_worker, execute it and handle response
64-
network.delayed_worker(query_worker, self.response_base,
65-
self.exception_base)
65+
query_worker.start(self.response_base, self.exception_base)
6666

6767
def reject_base(self):
6868
if self.reject_handler:

0 commit comments

Comments
 (0)
Please sign in to comment.