Merge lp:~ben-hutchings/ensoft-sextant/filter-search into lp:ensoft-sextant
- filter-search
- Merge into whiteline
Status: | Superseded |
---|---|
Proposed branch: | lp:~ben-hutchings/ensoft-sextant/filter-search |
Merge into: | lp:ensoft-sextant |
Prerequisite: | lp:~ben-hutchings/ensoft-sextant/autocomplete-fix |
Diff against target: |
696 lines (+239/-112) 8 files modified
resources/sextant/web/interface.html (+2/-2) src/sextant/__main__.py (+8/-5) src/sextant/db_api.py (+118/-59) src/sextant/export.py (+1/-1) src/sextant/objdump_parser.py (+82/-33) src/sextant/test_parser.py (+1/-1) src/sextant/update_db.py (+15/-8) src/sextant/web/server.py (+12/-3) |
To merge this branch: | bzr merge lp:~ben-hutchings/ensoft-sextant/filter-search |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert | Approve | ||
Review via email: mp+242182@code.launchpad.net |
This proposal supersedes a proposal from 2014-11-19.
This proposal has been superseded by a proposal from 2014-11-21.
Commit message
Function name search within the web frontend now supports extended syntax:
'<name matches>:<file path matches>'
where name matches and file path matches are (possibly) comma separated lists, and may include wildcards '.*'. At least one of the two must be specified.
Fixed bug with inline functions being uploaded multiple times into the database.
Fixed bug with over-zealous name stripping of function identifiers.
Fixed bug by which some functions were not uploaded.
Note: some tests do not pass - this is not because it doesn't work! Some details have changed under the hood which require the tests to be changed.
Description of the change
Function name search within the web frontend now supports extended syntax:
'<name matches>:<file path matches>'
where name matches and file path matches are (possibly) comma separated lists, and may include wildcards '.*'. At least one of the two must be specified.
Fixed bug with inline functions being uploaded multiple times into the database.
Fixed bug with over-zealous name stripping of function identifiers.
Fixed bug by which some functions were not uploaded.
Note: some tests do not pass - this is not because it doesn't work! Some details have changed under the hood which require the tests to be changed.
Robert (rjwills) : | # |
Martin Morrison (isoschiz) wrote : | # |
- 48. By Ben Hutchings
-
merge from autocomplete-fix
- 49. By Ben Hutchings
-
another merge from autocomplete-fix
- 50. By Ben Hutchings
-
fixed bug causing extrac characters to be removed from the start of symbol names
Unmerged revisions
Preview Diff
1 | === modified file 'resources/sextant/web/interface.html' | |||
2 | --- resources/sextant/web/interface.html 2014-11-21 12:34:24 +0000 | |||
3 | +++ resources/sextant/web/interface.html 2014-11-21 12:34:25 +0000 | |||
4 | @@ -27,8 +27,8 @@ | |||
5 | 27 | All functions calling specific function</option> | 27 | All functions calling specific function</option> |
6 | 28 | <option value="functions_called_by"> | 28 | <option value="functions_called_by"> |
7 | 29 | All functions called by a specific function</option> | 29 | All functions called by a specific function</option> |
10 | 30 | <option value="all_call_paths"> | 30 | <!--option value="all_call_paths"> REMOVED AS THIS IS SLOW FOR IOS |
11 | 31 | All function call paths between two functions</option> | 31 | All function call paths between two functions</option--> |
12 | 32 | <option value="shortest_call_path"> | 32 | <option value="shortest_call_path"> |
13 | 33 | Shortest path between two functions</option> | 33 | Shortest path between two functions</option> |
14 | 34 | <option value="function_names"> | 34 | <option value="function_names"> |
15 | 35 | 35 | ||
16 | === modified file 'src/sextant/__main__.py' | |||
17 | --- src/sextant/__main__.py 2014-10-17 15:30:14 +0000 | |||
18 | +++ src/sextant/__main__.py 2014-11-21 12:34:25 +0000 | |||
19 | @@ -127,16 +127,13 @@ | |||
20 | 127 | except TypeError: | 127 | except TypeError: |
21 | 128 | alternative_name = None | 128 | alternative_name = None |
22 | 129 | 129 | ||
23 | 130 | not_object_file = args.not_object_file | ||
24 | 131 | # the default is "yes, this is an object file" if not-object-file was | ||
25 | 132 | # unsupplied | ||
26 | 133 | |||
27 | 134 | try: | 130 | try: |
28 | 135 | update_db.upload_program(connection, | 131 | update_db.upload_program(connection, |
29 | 136 | getpass.getuser(), | 132 | getpass.getuser(), |
30 | 137 | args.input_file, | 133 | args.input_file, |
31 | 138 | alternative_name, | 134 | alternative_name, |
33 | 139 | not_object_file) | 135 | args.not_object_file, |
34 | 136 | args.add_file_paths) | ||
35 | 140 | except requests.exceptions.ConnectionError as e: | 137 | except requests.exceptions.ConnectionError as e: |
36 | 141 | msg = 'Connection error to server {}: {}' | 138 | msg = 'Connection error to server {}: {}' |
37 | 142 | logging.error(msg.format(_displayable_url(args), e)) | 139 | logging.error(msg.format(_displayable_url(args), e)) |
38 | @@ -221,6 +218,12 @@ | |||
39 | 221 | help='default False, if the input file is an ' | 218 | help='default False, if the input file is an ' |
40 | 222 | 'object to be disassembled', | 219 | 'object to be disassembled', |
41 | 223 | action='store_true') | 220 | action='store_true') |
42 | 221 | parsers['add'].add_argument('--add-file-paths', | ||
43 | 222 | help='default False, set to True to make objdump ' | ||
44 | 223 | 'extract the file paths for each function. ' | ||
45 | 224 | 'WARNING: this is SLOW for large object files, ' | ||
46 | 225 | '~15 hours for IOS.', | ||
47 | 226 | action='store_true') | ||
48 | 224 | 227 | ||
49 | 225 | parsers['delete'] = subparsers.add_parser('delete-program', | 228 | parsers['delete'] = subparsers.add_parser('delete-program', |
50 | 226 | help="delete a program from the database") | 229 | help="delete a program from the database") |
51 | 227 | 230 | ||
52 | === modified file 'src/sextant/db_api.py' | |||
53 | --- src/sextant/db_api.py 2014-11-21 12:34:24 +0000 | |||
54 | +++ src/sextant/db_api.py 2014-11-21 12:34:25 +0000 | |||
55 | @@ -159,7 +159,7 @@ | |||
56 | 159 | tmp_path = os.path.join(self._tmp_dir, '{}_{{}}'.format(program_name)) | 159 | tmp_path = os.path.join(self._tmp_dir, '{}_{{}}'.format(program_name)) |
57 | 160 | 160 | ||
58 | 161 | self.func_writer = CSVWriter(tmp_path.format('funcs'), | 161 | self.func_writer = CSVWriter(tmp_path.format('funcs'), |
60 | 162 | headers=['name', 'type'], | 162 | headers=['name', 'type', 'file'], |
61 | 163 | max_rows=5000) | 163 | max_rows=5000) |
62 | 164 | self.call_writer = CSVWriter(tmp_path.format('calls'), | 164 | self.call_writer = CSVWriter(tmp_path.format('calls'), |
63 | 165 | headers=['caller', 'callee'], | 165 | headers=['caller', 'callee'], |
64 | @@ -171,7 +171,7 @@ | |||
65 | 171 | ' WITH line, toInt(line.id) as lineid' | 171 | ' WITH line, toInt(line.id) as lineid' |
66 | 172 | ' MATCH (n:program {{name: "{}"}})' | 172 | ' MATCH (n:program {{name: "{}"}})' |
67 | 173 | ' CREATE (n)-[:subject]->(m:func {{name: line.name,' | 173 | ' CREATE (n)-[:subject]->(m:func {{name: line.name,' |
69 | 174 | ' id: lineid, type: line.type}})') | 174 | ' id: lineid, type: line.type, file: line.file}})') |
70 | 175 | 175 | ||
71 | 176 | self.add_call_query = (' USING PERIODIC COMMIT 250' | 176 | self.add_call_query = (' USING PERIODIC COMMIT 250' |
72 | 177 | ' LOAD CSV WITH HEADERS FROM "file:{}" AS line' | 177 | ' LOAD CSV WITH HEADERS FROM "file:{}" AS line' |
73 | @@ -203,7 +203,7 @@ | |||
74 | 203 | # Propagate the error if there is one. | 203 | # Propagate the error if there is one. |
75 | 204 | return False if etype is not None else True | 204 | return False if etype is not None else True |
76 | 205 | 205 | ||
78 | 206 | def add_function(self, name, typ='normal'): | 206 | def add_function(self, name, typ='normal', source='unknown'): |
79 | 207 | """ | 207 | """ |
80 | 208 | Add a function. | 208 | Add a function. |
81 | 209 | 209 | ||
82 | @@ -219,7 +219,7 @@ | |||
83 | 219 | pointer: we know only that the function exists, not its | 219 | pointer: we know only that the function exists, not its |
84 | 220 | name or details. | 220 | name or details. |
85 | 221 | """ | 221 | """ |
87 | 222 | self.func_writer.write(name, typ) | 222 | self.func_writer.write(name, typ, source) |
88 | 223 | 223 | ||
89 | 224 | def add_call(self, caller, callee): | 224 | def add_call(self, caller, callee): |
90 | 225 | """ | 225 | """ |
91 | @@ -257,6 +257,19 @@ | |||
92 | 257 | remote_paths: | 257 | remote_paths: |
93 | 258 | A list of the paths of the remote fils. | 258 | A list of the paths of the remote fils. |
94 | 259 | """ | 259 | """ |
95 | 260 | |||
96 | 261 | def try_rmdir(path): | ||
97 | 262 | # Helper function to try and remove a directory, silently | ||
98 | 263 | # fail if it contains files, otherwise raise the exception. | ||
99 | 264 | try: | ||
100 | 265 | os.rmdir(path) | ||
101 | 266 | except OSError as e: | ||
102 | 267 | if e.errno in (os.errno.ENOTEMPTY, os.errno.ENOENT): | ||
103 | 268 | # Files in directory or directory doesn't exist. | ||
104 | 269 | pass | ||
105 | 270 | else: | ||
106 | 271 | raise e | ||
107 | 272 | |||
108 | 260 | print('Cleaning temporary files...', end='') | 273 | print('Cleaning temporary files...', end='') |
109 | 261 | file_paths = list(itertools.chain(self.func_writer.file_iter(), | 274 | file_paths = list(itertools.chain(self.func_writer.file_iter(), |
110 | 262 | self.call_writer.file_iter())) | 275 | self.call_writer.file_iter())) |
111 | @@ -264,16 +277,9 @@ | |||
112 | 264 | for path in file_paths: | 277 | for path in file_paths: |
113 | 265 | os.remove(path) | 278 | os.remove(path) |
114 | 266 | 279 | ||
125 | 267 | os.rmdir(self._tmp_dir) | 280 | try_rmdir(self._tmp_dir) |
126 | 268 | 281 | try_rmdir(TMP_DIR) | |
127 | 269 | try: | 282 | |
118 | 270 | # If the parent sextant temp folder is empty, remove it. | ||
119 | 271 | os.rmdir(TMP_DIR) | ||
120 | 272 | except: | ||
121 | 273 | # There is other stuff in TMP_DIR (i.e. from other users), so | ||
122 | 274 | # leave it. | ||
123 | 275 | pass | ||
124 | 276 | |||
128 | 277 | self._ssh.remove_from_tmp_dir(remote_paths) | 283 | self._ssh.remove_from_tmp_dir(remote_paths) |
129 | 278 | 284 | ||
130 | 279 | print('done.') | 285 | print('done.') |
131 | @@ -290,6 +296,7 @@ | |||
132 | 290 | 296 | ||
133 | 291 | tx.append('CREATE CONSTRAINT ON (p:program) ASSERT p.name IS UNIQUE') | 297 | tx.append('CREATE CONSTRAINT ON (p:program) ASSERT p.name IS UNIQUE') |
134 | 292 | tx.append('CREATE INDEX ON :func(name)') | 298 | tx.append('CREATE INDEX ON :func(name)') |
135 | 299 | tx.append('CREATE INDEX ON: func(file)') | ||
136 | 293 | 300 | ||
137 | 294 | # Apply the transaction. | 301 | # Apply the transaction. |
138 | 295 | tx.commit() | 302 | tx.commit() |
139 | @@ -832,7 +839,7 @@ | |||
140 | 832 | result = self._db.query(q, returns=neo4jrestclient.Node) | 839 | result = self._db.query(q, returns=neo4jrestclient.Node) |
141 | 833 | return bool(result) | 840 | return bool(result) |
142 | 834 | 841 | ||
144 | 835 | def get_function_names(self, program_name, search, max_funcs): | 842 | def get_function_names(self, program_name, search=None, max_funcs=None): |
145 | 836 | """ | 843 | """ |
146 | 837 | Execute query to retrieve a list of all functions in the program. | 844 | Execute query to retrieve a list of all functions in the program. |
147 | 838 | Any of the output names can be used verbatim in any SextantConnection | 845 | Any of the output names can be used verbatim in any SextantConnection |
148 | @@ -845,15 +852,82 @@ | |||
149 | 845 | if not validate_query(program_name): | 852 | if not validate_query(program_name): |
150 | 846 | return set() | 853 | return set() |
151 | 847 | 854 | ||
152 | 855 | limit = "LIMIT {}".format(max_funcs) if max_funcs else "" | ||
153 | 856 | |||
154 | 848 | if not search: | 857 | if not search: |
155 | 849 | q = (' MATCH (:program {{name: "{}"}})-[:subject]->(f:func)' | 858 | q = (' MATCH (:program {{name: "{}"}})-[:subject]->(f:func)' |
157 | 850 | ' RETURN f.name LIMIT {}').format(program_name, max_funcs) | 859 | ' RETURN f.name {}').format(program_name, limit) |
158 | 851 | else: | 860 | else: |
159 | 852 | q = (' MATCH (:program {{name: "{}"}})-[:subject]->(f:func)' | 861 | q = (' MATCH (:program {{name: "{}"}})-[:subject]->(f:func)' |
162 | 853 | ' WHERE f.name =~ ".*{}.*" RETURN f.name LIMIT {}' | 862 | ' WHERE f.name =~ ".*{}.*" RETURN f.name {}' |
163 | 854 | .format(program_name, search, max_funcs)) | 863 | .format(program_name, search, limit)) |
164 | 855 | return {func[0] for func in self._db.query(q)} | 864 | return {func[0] for func in self._db.query(q)} |
165 | 856 | 865 | ||
166 | 866 | @staticmethod | ||
167 | 867 | def get_query(identifier, search): | ||
168 | 868 | """ | ||
169 | 869 | Builds a filter query from a search pattern which may contain commas | ||
170 | 870 | and/or wildcards. | ||
171 | 871 | |||
172 | 872 | Return: | ||
173 | 873 | string: part of a valid cypher query. | ||
174 | 874 | Arguments: | ||
175 | 875 | identifier: | ||
176 | 876 | The identifier of the node whose properties to filter on, | ||
177 | 877 | e.g. 'f' after a 'MATCH (f:func) ...' | ||
178 | 878 | search: | ||
179 | 879 | The pattern to build the search from, of form: | ||
180 | 880 | '<name patterns>:<path patterns>' | ||
181 | 881 | where patterns are possibly empty, possibly comma separated | ||
182 | 882 | lists of strings, which will be compared to the 'name' and | ||
183 | 883 | 'file' (path) attributes of 'identifier'. | ||
184 | 884 | |||
185 | 885 | These strings may contain wildcards: e.g: | ||
186 | 886 | .*substring.* | ||
187 | 887 | sub.*string | ||
188 | 888 | etc. | ||
189 | 889 | |||
190 | 890 | """ | ||
191 | 891 | if ':' in search: | ||
192 | 892 | func_subs, file_subs = search.split(':') | ||
193 | 893 | else: | ||
194 | 894 | func_subs, file_subs = search, '' | ||
195 | 895 | |||
196 | 896 | # Remove empty strings. | ||
197 | 897 | func_subs = [sub for sub in func_subs.split(',') if sub] | ||
198 | 898 | file_subs = [sub for sub in file_subs.split(',') if sub] | ||
199 | 899 | |||
200 | 900 | # Cases for search: | ||
201 | 901 | # <specific name>:<redundant stuff> | ||
202 | 902 | # <wildcard name>:<specific filepath> | ||
203 | 903 | # <wildcard name>:<wildcard filepath> | ||
204 | 904 | |||
205 | 905 | query_str = "" | ||
206 | 906 | |||
207 | 907 | def get_list(subs): | ||
208 | 908 | return '[{}]'.format(','.join("'{}'".format(s) for s in subs)) | ||
209 | 909 | |||
210 | 910 | |||
211 | 911 | if func_subs and not any('*' in sub for sub in func_subs): | ||
212 | 912 | # List of specific functions. Don't care about anything after ':' | ||
213 | 913 | query_str += ('USING INDEX {0}:func(name) WHERE {0}.name IN {1} ' | ||
214 | 914 | .format(identifier, get_list(func_subs))) | ||
215 | 915 | else: | ||
216 | 916 | if file_subs and not any('*' in sub for sub in file_subs): | ||
217 | 917 | # Specific file to look in. | ||
218 | 918 | query_str = ('USING INDEX {0}.func(file) WHERE {0}.file IN {1} ' | ||
219 | 919 | .format(identifier, get_list(file_subs))) | ||
220 | 920 | elif file_subs: | ||
221 | 921 | query_str = ('WHERE ANY (s_file IN {} WHERE {}.file =~ s_file) ' | ||
222 | 922 | .format(get_list(file_subs), identifier)) | ||
223 | 923 | |||
224 | 924 | if func_subs: | ||
225 | 925 | query_str += 'AND ' if file_subs else 'WHERE ' | ||
226 | 926 | query_str += ('ANY (s_name IN {} WHERE {}.name =~ s_name) ' | ||
227 | 927 | .format(get_list(func_subs), identifier)) | ||
228 | 928 | |||
229 | 929 | return query_str | ||
230 | 930 | |||
231 | 857 | def get_all_functions_called(self, program_name, function_calling): | 931 | def get_all_functions_called(self, program_name, function_calling): |
232 | 858 | """ | 932 | """ |
233 | 859 | Execute query to find all functions called by a function (indirectly). | 933 | Execute query to find all functions called by a function (indirectly). |
234 | @@ -863,14 +937,9 @@ | |||
235 | 863 | :param function_calling: string name of a function whose children to find | 937 | :param function_calling: string name of a function whose children to find |
236 | 864 | :return: FunctionQueryResult, maximal subgraph rooted at function_calling | 938 | :return: FunctionQueryResult, maximal subgraph rooted at function_calling |
237 | 865 | """ | 939 | """ |
246 | 866 | 940 | q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(f:func) {}' | |
247 | 867 | if not self.check_function_exists(program_name, function_calling): | 941 | ' MATCH (f)-[:calls]->(g:func) RETURN distinct f, g' |
248 | 868 | return None | 942 | .format(program_name, SextantConnection.get_query('f', function_calling))) |
241 | 869 | |||
242 | 870 | q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(f:func {{name: "{}"}})' | ||
243 | 871 | ' USING INDEX f:func(name)' | ||
244 | 872 | ' MATCH (f)-[:calls*]->(g) RETURN distinct f, g' | ||
245 | 873 | .format(program_name, function_calling)) | ||
249 | 874 | 943 | ||
250 | 875 | return self._execute_query(program_name, q) | 944 | return self._execute_query(program_name, q) |
251 | 876 | 945 | ||
252 | @@ -884,14 +953,10 @@ | |||
253 | 884 | :return: FunctionQueryResult, maximal connected subgraph with leaf function_called | 953 | :return: FunctionQueryResult, maximal connected subgraph with leaf function_called |
254 | 885 | """ | 954 | """ |
255 | 886 | 955 | ||
264 | 887 | if not self.check_function_exists(program_name, function_called): | 956 | q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(g:func) {}' |
265 | 888 | return None | 957 | ' MATCH (f)-[:calls]->(g)' |
266 | 889 | 958 | ' RETURN distinct f, g') | |
267 | 890 | q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(g:func {{name: "{}"}})' | 959 | q = q.format(program_name, SextantConnection.get_query('g', function_called), program_name) |
260 | 891 | ' USING INDEX g:func(name)' | ||
261 | 892 | ' MATCH (f)-[:calls*]->(g) WHERE f.name <> "{}"' | ||
262 | 893 | ' RETURN distinct f , g') | ||
263 | 894 | q = q.format(program_name, function_called, program_name) | ||
268 | 895 | 960 | ||
269 | 896 | return self._execute_query(program_name, q) | 961 | return self._execute_query(program_name, q) |
270 | 897 | 962 | ||
271 | @@ -910,22 +975,17 @@ | |||
272 | 910 | if not self.check_program_exists(program_name): | 975 | if not self.check_program_exists(program_name): |
273 | 911 | return None | 976 | return None |
274 | 912 | 977 | ||
285 | 913 | if not self.check_function_exists(program_name, function_called): | 978 | start_q = SextantConnection.get_query('start', function_calling) |
286 | 914 | return None | 979 | end_q = SextantConnection.get_query('end', function_called) |
287 | 915 | 980 | ||
288 | 916 | if not self.check_function_exists(program_name, function_calling): | 981 | q = (' MATCH (p:program {{name: "{}"}})' |
289 | 917 | return None | 982 | ' MATCH (p)-[:subject]->(start:func) {} WITH start, p' |
290 | 918 | 983 | ' MATCH (p)-[:subject]->(end:func) {} WITH start, end' | |
281 | 919 | q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(start:func {{name: "{}"}})' | ||
282 | 920 | ' USING INDEX start:func(name)' | ||
283 | 921 | ' MATCH (p)-[:subject]->(end:func {{name: "{}"}})' | ||
284 | 922 | ' USING INDEX end:func(name)' | ||
291 | 923 | ' MATCH path=(start)-[:calls*]->(end)' | 984 | ' MATCH path=(start)-[:calls*]->(end)' |
292 | 924 | ' WITH DISTINCT nodes(path) AS result' | 985 | ' WITH DISTINCT nodes(path) AS result' |
293 | 925 | ' UNWIND result AS answer' | 986 | ' UNWIND result AS answer' |
294 | 926 | ' RETURN answer') | 987 | ' RETURN answer') |
297 | 927 | q = q.format(program_name, function_calling, function_called) | 988 | q = q.format(program_name, start_q, end_q) |
296 | 928 | |||
298 | 929 | return self._execute_query(program_name, q) | 989 | return self._execute_query(program_name, q) |
299 | 930 | 990 | ||
300 | 931 | def get_whole_program(self, program_name): | 991 | def get_whole_program(self, program_name): |
301 | @@ -942,7 +1002,7 @@ | |||
302 | 942 | ' RETURN (f)'.format(program_name)) | 1002 | ' RETURN (f)'.format(program_name)) |
303 | 943 | return self._execute_query(program_name, q) | 1003 | return self._execute_query(program_name, q) |
304 | 944 | 1004 | ||
306 | 945 | def get_shortest_path_between_functions(self, program_name, func1, func2): | 1005 | def get_shortest_path_between_functions(self, program_name, function_calling, function_called): |
307 | 946 | """ | 1006 | """ |
308 | 947 | Execute query to get a single, shortest, path between two functions. | 1007 | Execute query to get a single, shortest, path between two functions. |
309 | 948 | :param program_name: string name of the program we wish to search under | 1008 | :param program_name: string name of the program we wish to search under |
310 | @@ -953,17 +1013,16 @@ | |||
311 | 953 | if not self.check_program_exists(program_name): | 1013 | if not self.check_program_exists(program_name): |
312 | 954 | return None | 1014 | return None |
313 | 955 | 1015 | ||
326 | 956 | if not self.check_function_exists(program_name, func1): | 1016 | start_q = SextantConnection.get_query('start', function_calling) |
327 | 957 | return None | 1017 | end_q = SextantConnection.get_query('end', function_called) |
328 | 958 | 1018 | ||
329 | 959 | if not self.check_function_exists(program_name, func2): | 1019 | q = (' MATCH (p:program {{name: "{}"}})' |
330 | 960 | return None | 1020 | ' MATCH (p)-[:subject]->(start:func) {} WITH start, p' |
331 | 961 | 1021 | ' MATCH (p)-[:subject]->(end:func) {} WITH start, end' | |
332 | 962 | q = (' MATCH (p:program {{name: "{}"}})-[:subject]->(f:func {{name: "{}"}})' | 1022 | ' MATCH path=shortestPath((start)-[:calls*]->(end))' |
333 | 963 | ' USING INDEX f:func(name)' | 1023 | ' UNWIND nodes(path) AS answer' |
334 | 964 | ' MATCH (p)-[:subject]->(g:func {{name: "{}"}})' | 1024 | ' RETURN answer') |
335 | 965 | ' MATCH path=shortestPath((f)-[:calls*]->(g))' | 1025 | q = q.format(program_name, start_q, end_q) |
324 | 966 | ' UNWIND nodes(path) AS ans' | ||
325 | 967 | ' RETURN ans'.format(program_name, func1, func2)) | ||
336 | 968 | 1026 | ||
337 | 969 | return self._execute_query(program_name, q) | 1027 | return self._execute_query(program_name, q) |
338 | 1028 | |||
339 | 970 | 1029 | ||
340 | === modified file 'src/sextant/export.py' | |||
341 | --- src/sextant/export.py 2014-10-13 14:58:12 +0000 | |||
342 | +++ src/sextant/export.py 2014-11-21 12:34:25 +0000 | |||
343 | @@ -48,7 +48,7 @@ | |||
344 | 48 | for func in program.get_functions(): | 48 | for func in program.get_functions(): |
345 | 49 | if func.type == "stub": | 49 | if func.type == "stub": |
346 | 50 | output_str += ' "{}" [fillcolor=pink, style=filled]\n'.format(func.name) | 50 | output_str += ' "{}" [fillcolor=pink, style=filled]\n'.format(func.name) |
348 | 51 | elif func.type == "function_pointer": | 51 | elif func.type == "pointer": |
349 | 52 | output_str += ' "{}" [fillcolor=yellow, style=filled]\n'.format(func.name) | 52 | output_str += ' "{}" [fillcolor=yellow, style=filled]\n'.format(func.name) |
350 | 53 | 53 | ||
351 | 54 | # in all cases, even if we've specified that we want a filled-in | 54 | # in all cases, even if we've specified that we want a filled-in |
352 | 55 | 55 | ||
353 | === modified file 'src/sextant/objdump_parser.py' (properties changed: +x to -x) | |||
354 | --- src/sextant/objdump_parser.py 2014-10-23 11:15:48 +0000 | |||
355 | +++ src/sextant/objdump_parser.py 2014-11-21 12:34:25 +0000 | |||
356 | @@ -42,9 +42,12 @@ | |||
357 | 42 | The number of function calls that have been parsed. | 42 | The number of function calls that have been parsed. |
358 | 43 | function_ptr_count: | 43 | function_ptr_count: |
359 | 44 | The number of function pointers that have been detected. | 44 | The number of function pointers that have been detected. |
363 | 45 | _known_stubs: | 45 | _known_functions: |
364 | 46 | A set of the names of functions with type 'stub' that have been | 46 | A set of the names of functions that have been |
365 | 47 | parsed - used to avoid registering a stub multiple times. | 47 | parsed - used to avoid registering a function multiple times. |
366 | 48 | _partial_functions: | ||
367 | 49 | A set of functions whose names we have seen but whose source | ||
368 | 50 | files we don't yet know. | ||
369 | 48 | 51 | ||
370 | 49 | """ | 52 | """ |
371 | 50 | def __init__(self, file_path, file_object=None, | 53 | def __init__(self, file_path, file_object=None, |
372 | @@ -102,13 +105,14 @@ | |||
373 | 102 | self.call_count = 0 | 105 | self.call_count = 0 |
374 | 103 | self.function_ptr_count = 0 | 106 | self.function_ptr_count = 0 |
375 | 104 | 107 | ||
379 | 105 | # Avoid adding duplicate function stubs (as these are detected from | 108 | # Avoid adding duplicate functions. |
380 | 106 | # function calls so may be repeated). | 109 | self._known_functions = set() |
381 | 107 | self._known_stubs = set() | 110 | # Set of partially-parsed functions. |
382 | 111 | self._partial_functions = set() | ||
383 | 108 | 112 | ||
384 | 109 | # By default print information to stdout. | 113 | # By default print information to stdout. |
387 | 110 | def print_func(name, typ): | 114 | def print_func(name, typ, source='unknown'): |
388 | 111 | print('func {:25}{}'.format(name, typ)) | 115 | print('func {:25}{:15}{}'.format(name, typ, source)) |
389 | 112 | 116 | ||
390 | 113 | def print_call(caller, callee): | 117 | def print_call(caller, callee): |
391 | 114 | print('call {:25}{:25}'.format(caller, callee)) | 118 | print('call {:25}{:25}'.format(caller, callee)) |
392 | @@ -116,7 +120,6 @@ | |||
393 | 116 | def print_started(parser): | 120 | def print_started(parser): |
394 | 117 | print('parse started: {}[{}]'.format(self.path, ', '.join(self.sections))) | 121 | print('parse started: {}[{}]'.format(self.path, ', '.join(self.sections))) |
395 | 118 | 122 | ||
396 | 119 | |||
397 | 120 | def print_finished(parser): | 123 | def print_finished(parser): |
398 | 121 | print('parsed {} functions and {} calls'.format(self.function_count, self.call_count)) | 124 | print('parsed {} functions and {} calls'.format(self.function_count, self.call_count)) |
399 | 122 | 125 | ||
400 | @@ -134,12 +137,32 @@ | |||
401 | 134 | self.function_ptr_count += 1 | 137 | self.function_ptr_count += 1 |
402 | 135 | return name | 138 | return name |
403 | 136 | 139 | ||
410 | 137 | def _add_function_normal(self, name): | 140 | def _add_function(self, name, source=None): |
411 | 138 | """ | 141 | """ |
412 | 139 | Add a function which we have full assembly code for. | 142 | Add a partially known or fully known function. |
413 | 140 | """ | 143 | """ |
414 | 141 | self.add_function(name, 'normal') | 144 | if source is None: |
415 | 142 | self.function_count += 1 | 145 | # Partial definition - if do not already have a full definition |
416 | 146 | # for this name then add it to the partials set. | ||
417 | 147 | if not name in self._known_functions: | ||
418 | 148 | self._partial_functions.add(name) | ||
419 | 149 | elif source == 'unknown': | ||
420 | 150 | # Manually adding a stub function. | ||
421 | 151 | self.add_function(name, 'stub', source) | ||
422 | 152 | self.function_count += 1 | ||
423 | 153 | elif name not in self._known_functions: | ||
424 | 154 | # A full definition - either upgrade from partial function | ||
425 | 155 | # to known function, or add directly to known functions | ||
426 | 156 | # (otherwise we have already seen it) | ||
427 | 157 | |||
428 | 158 | try: | ||
429 | 159 | self._partial_functions.remove(name) | ||
430 | 160 | except KeyError: | ||
431 | 161 | pass | ||
432 | 162 | |||
433 | 163 | self._known_functions.add(name) | ||
434 | 164 | self.add_function(name, 'normal', source) | ||
435 | 165 | self.function_count += 1 | ||
436 | 143 | 166 | ||
437 | 144 | def _add_function_ptr(self, name): | 167 | def _add_function_ptr(self, name): |
438 | 145 | """ | 168 | """ |
439 | @@ -148,15 +171,6 @@ | |||
440 | 148 | self.add_function(name, 'pointer') | 171 | self.add_function(name, 'pointer') |
441 | 149 | self.function_count += 1 | 172 | self.function_count += 1 |
442 | 150 | 173 | ||
443 | 151 | def _add_function_stub(self, name): | ||
444 | 152 | """ | ||
445 | 153 | Add a function stub - we have its name but none of its internals. | ||
446 | 154 | """ | ||
447 | 155 | if not name in self._known_stubs: | ||
448 | 156 | self._known_stubs.add(name) | ||
449 | 157 | self.add_function(name, 'stub') | ||
450 | 158 | self.function_count += 1 | ||
451 | 159 | |||
452 | 160 | def _add_call(self, caller, callee): | 174 | def _add_call(self, caller, callee): |
453 | 161 | """ | 175 | """ |
454 | 162 | Add a function call from caller to callee. | 176 | Add a function call from caller to callee. |
455 | @@ -171,10 +185,20 @@ | |||
456 | 171 | self.started() | 185 | self.started() |
457 | 172 | 186 | ||
458 | 173 | if self._file is not None: | 187 | if self._file is not None: |
461 | 174 | in_section = False # if we are in one of self.sections | 188 | in_section = False # If we are in one of self.sections. |
462 | 175 | current_function = None # track the caller for function calls | 189 | current_function = None # Track the caller for function calls. |
463 | 190 | to_add = False | ||
464 | 176 | 191 | ||
465 | 177 | for line in self._file: | 192 | for line in self._file: |
466 | 193 | if to_add: | ||
467 | 194 | file_line = line.startswith('/') | ||
468 | 195 | source = line.split(':')[0] if file_line else None | ||
469 | 196 | self._add_function(current_function, source) | ||
470 | 197 | to_add = False | ||
471 | 198 | |||
472 | 199 | if file_line: | ||
473 | 200 | continue | ||
474 | 201 | |||
475 | 178 | if line.startswith('Disassembly'): | 202 | if line.startswith('Disassembly'): |
476 | 179 | # 'Disassembly of section <name>:\n' | 203 | # 'Disassembly of section <name>:\n' |
477 | 180 | section = line.split(' ')[-1].rstrip(':\n') | 204 | section = line.split(' ')[-1].rstrip(':\n') |
478 | @@ -189,12 +213,19 @@ | |||
479 | 189 | # <function_name>[@plt] | 213 | # <function_name>[@plt] |
480 | 190 | function_identifier = line.split('<')[-1].split('>')[0] | 214 | function_identifier = line.split('<')[-1].split('>')[0] |
481 | 191 | 215 | ||
482 | 216 | # IOS builds add a __be_ (big endian) prefix to all functions, | ||
483 | 217 | # get rid of it if it is there, | ||
484 | 218 | if function_identifier.startswith('__be_'): | ||
485 | 219 | function_identifier = function_identifier.lstrip('__be_') | ||
486 | 220 | |||
487 | 192 | if '@' in function_identifier: | 221 | if '@' in function_identifier: |
488 | 222 | # Of form <function name>@<other stuff>. | ||
489 | 193 | current_function = function_identifier.split('@')[0] | 223 | current_function = function_identifier.split('@')[0] |
491 | 194 | self._add_function_stub(current_function) | 224 | self._add_function(current_function) |
492 | 195 | else: | 225 | else: |
493 | 196 | current_function = function_identifier | 226 | current_function = function_identifier |
495 | 197 | self._add_function_normal(current_function) | 227 | # Flag function - we look for source on the next line. |
496 | 228 | to_add = True | ||
497 | 198 | 229 | ||
498 | 199 | elif 'call ' in line or 'callq ' in line: | 230 | elif 'call ' in line or 'callq ' in line: |
499 | 200 | # WHITESPACE to prevent picking up function names | 231 | # WHITESPACE to prevent picking up function names |
500 | @@ -213,9 +244,12 @@ | |||
501 | 213 | # from which we extract name | 244 | # from which we extract name |
502 | 214 | callee_is_ptr = False | 245 | callee_is_ptr = False |
503 | 215 | function_identifier = callee_info.lstrip('<').rstrip('>\n') | 246 | function_identifier = callee_info.lstrip('<').rstrip('>\n') |
504 | 247 | if function_identifier.startswith('__be_'): | ||
505 | 248 | function_identifier = function_identifier.lstrip('__be_') | ||
506 | 249 | |||
507 | 216 | if '@' in function_identifier: | 250 | if '@' in function_identifier: |
508 | 217 | callee = function_identifier.split('@')[0] | 251 | callee = function_identifier.split('@')[0] |
510 | 218 | self._add_function_stub(callee) | 252 | self._add_function(callee) |
511 | 219 | else: | 253 | else: |
512 | 220 | callee = function_identifier.split('-')[-1].split('+')[0] | 254 | callee = function_identifier.split('-')[-1].split('+')[0] |
513 | 221 | # Do not add this fn now - it is a normal func | 255 | # Do not add this fn now - it is a normal func |
514 | @@ -231,6 +265,10 @@ | |||
515 | 231 | # Add the call. | 265 | # Add the call. |
516 | 232 | if not (self.ignore_ptrs and callee_is_ptr): | 266 | if not (self.ignore_ptrs and callee_is_ptr): |
517 | 233 | self._add_call(current_function, callee) | 267 | self._add_call(current_function, callee) |
518 | 268 | |||
519 | 269 | for name in self._partial_functions: | ||
520 | 270 | self._add_function(name, 'unknown') | ||
521 | 271 | |||
522 | 234 | 272 | ||
523 | 235 | self.finished() | 273 | self.finished() |
524 | 236 | 274 | ||
525 | @@ -261,7 +299,7 @@ | |||
526 | 261 | return result | 299 | return result |
527 | 262 | 300 | ||
528 | 263 | 301 | ||
530 | 264 | def run_objdump(input_file): | 302 | def run_objdump(input_file, add_file_paths=False): |
531 | 265 | """ | 303 | """ |
532 | 266 | Run the objdump command on the file with the given path. | 304 | Run the objdump command on the file with the given path. |
533 | 267 | 305 | ||
534 | @@ -271,13 +309,24 @@ | |||
535 | 271 | Arguments: | 309 | Arguments: |
536 | 272 | input_file: | 310 | input_file: |
537 | 273 | The path of the file to run objdump on. | 311 | The path of the file to run objdump on. |
538 | 312 | add_file_paths: | ||
539 | 313 | Whether to call with -l option to extract line numbers and source | ||
540 | 314 | files from the binary. VERY SLOW on large binaries (~15 hours for ios). | ||
541 | 274 | 315 | ||
542 | 275 | """ | 316 | """ |
543 | 317 | print('input file: {}'.format(input_file)) | ||
544 | 276 | # A single section can be specified for parsing with the -j flag, | 318 | # A single section can be specified for parsing with the -j flag, |
545 | 277 | # but it is not obviously possible to parse multiple sections like this. | 319 | # but it is not obviously possible to parse multiple sections like this. |
549 | 278 | p = subprocess.Popen(['objdump', '-d', input_file, '--no-show-raw-insn'], | 320 | args = ['objdump', '-d', input_file, '--no-show-raw-insn'] |
550 | 279 | stdout=subprocess.PIPE) | 321 | if add_file_paths: |
551 | 280 | g = subprocess.Popen(['egrep', 'Disassembly|call(q)? |>:$'], stdin=p.stdout, stdout=subprocess.PIPE) | 322 | args += ['--line-numbers'] |
552 | 323 | |||
553 | 324 | p = subprocess.Popen(args, stdout=subprocess.PIPE) | ||
554 | 325 | # Egrep filters out the section headers (Disassembly of section...), | ||
555 | 326 | # the call lines (... [l]call[q] ...), the function declarations | ||
556 | 327 | # (... <function>:$) and the file paths (^/file_path). | ||
557 | 328 | g = subprocess.Popen(['egrep', 'Disassembly|call(q)? |>:$|^/'], | ||
558 | 329 | stdin=p.stdout, stdout=subprocess.PIPE) | ||
559 | 281 | return input_file, g.stdout | 330 | return input_file, g.stdout |
560 | 282 | 331 | ||
561 | 283 | 332 | ||
562 | 284 | 333 | ||
563 | === modified file 'src/sextant/test_parser.py' | |||
564 | --- src/sextant/test_parser.py 2014-10-23 11:15:48 +0000 | |||
565 | +++ src/sextant/test_parser.py 2014-11-21 12:34:25 +0000 | |||
566 | @@ -23,7 +23,7 @@ | |||
567 | 23 | calls = defaultdict(list) | 23 | calls = defaultdict(list) |
568 | 24 | 24 | ||
569 | 25 | # set the Parser to put output in local dictionaries | 25 | # set the Parser to put output in local dictionaries |
571 | 26 | add_function = lambda n, t: self.add_function(functions, n, t) | 26 | add_function = lambda n, t, s='unknown': self.add_function(functions, n, t) |
572 | 27 | add_call = lambda a, b: self.add_call(calls, a, b) | 27 | add_call = lambda a, b: self.add_call(calls, a, b) |
573 | 28 | 28 | ||
574 | 29 | p = parser.Parser(path, sections=sections, ignore_ptrs=ignore_ptrs, | 29 | p = parser.Parser(path, sections=sections, ignore_ptrs=ignore_ptrs, |
575 | 30 | 30 | ||
576 | === modified file 'src/sextant/test_resources/parser_test' | |||
577 | 31 | Binary files src/sextant/test_resources/parser_test 2014-10-13 14:10:01 +0000 and src/sextant/test_resources/parser_test 2014-11-21 12:34:25 +0000 differ | 31 | Binary files src/sextant/test_resources/parser_test 2014-10-13 14:10:01 +0000 and src/sextant/test_resources/parser_test 2014-11-21 12:34:25 +0000 differ |
578 | === modified file 'src/sextant/update_db.py' | |||
579 | --- src/sextant/update_db.py 2014-10-17 14:20:06 +0000 | |||
580 | +++ src/sextant/update_db.py 2014-11-21 12:34:25 +0000 | |||
581 | @@ -20,7 +20,7 @@ | |||
582 | 20 | import logging | 20 | import logging |
583 | 21 | 21 | ||
584 | 22 | def upload_program(connection, user_name, file_path, program_name=None, | 22 | def upload_program(connection, user_name, file_path, program_name=None, |
586 | 23 | not_object_file=False): | 23 | not_object_file=False, add_file_paths=False): |
587 | 24 | """ | 24 | """ |
588 | 25 | Upload a program's functions and call graph to the database. | 25 | Upload a program's functions and call graph to the database. |
589 | 26 | 26 | ||
590 | @@ -38,6 +38,9 @@ | |||
591 | 38 | not_object_file: | 38 | not_object_file: |
592 | 39 | Flag controlling whether file_path is pointing to a dump file or | 39 | Flag controlling whether file_path is pointing to a dump file or |
593 | 40 | a binary file. | 40 | a binary file. |
594 | 41 | add_file_paths: | ||
595 | 42 | Flag controlling whether to call objdump with the -l option to | ||
596 | 43 | extract line numbers and source files. VERY SLOW on large binaries. | ||
597 | 41 | """ | 44 | """ |
598 | 42 | if not connection._ssh: | 45 | if not connection._ssh: |
599 | 43 | raise SSHConnectionError('An SSH connection is required for ' | 46 | raise SSHConnectionError('An SSH connection is required for ' |
600 | @@ -59,9 +62,9 @@ | |||
601 | 59 | start = time() | 62 | start = time() |
602 | 60 | 63 | ||
603 | 61 | if not not_object_file: | 64 | if not not_object_file: |
605 | 62 | print('Generating dump file...', end='') | 65 | print('Generating dump file with{} file paths...'.format(('out', '')[add_file_paths]), end='') |
606 | 63 | sys.stdout.flush() | 66 | sys.stdout.flush() |
608 | 64 | file_path, file_object = run_objdump(file_path) | 67 | file_path, file_object = run_objdump(file_path, add_file_paths) |
609 | 65 | print('done.') | 68 | print('done.') |
610 | 66 | else: | 69 | else: |
611 | 67 | file_object = None | 70 | file_object = None |
612 | @@ -82,15 +85,19 @@ | |||
613 | 82 | print('done: {} functions and {} calls.' | 85 | print('done: {} functions and {} calls.' |
614 | 83 | .format(parser.function_count, parser.call_count)) | 86 | .format(parser.function_count, parser.call_count)) |
615 | 84 | 87 | ||
617 | 85 | parser = Parser(file_path = file_path, file_object = file_object, | 88 | parser = Parser(file_path=file_path, file_object = file_object, |
618 | 86 | sections=[], | 89 | sections=[], |
621 | 87 | add_function = program.add_function, | 90 | add_function=program.add_function, |
622 | 88 | add_call = program.add_call, | 91 | add_call=program.add_call, |
623 | 89 | started=lambda parser: start_parser(program), | 92 | started=lambda parser: start_parser(program), |
624 | 90 | finished=lambda parser: finish_parser(parser, program)) | 93 | finished=lambda parser: finish_parser(parser, program)) |
625 | 94 | |||
626 | 91 | parser.parse() | 95 | parser.parse() |
629 | 92 | 96 | ||
630 | 93 | program.commit() | 97 | if parser.function_count == 0: |
631 | 98 | print('Nothing to upload. Did you mean to add the --not-object-file flag?') | ||
632 | 99 | else: | ||
633 | 100 | program.commit() | ||
634 | 94 | 101 | ||
635 | 95 | end = time() | 102 | end = time() |
636 | 96 | print('Finished in {:.2f}s.'.format(end-start)) | 103 | print('Finished in {:.2f}s.'.format(end-start)) |
637 | 97 | 104 | ||
638 | === modified file 'src/sextant/web/server.py' | |||
639 | --- src/sextant/web/server.py 2014-11-21 12:34:24 +0000 | |||
640 | +++ src/sextant/web/server.py 2014-11-21 12:34:25 +0000 | |||
641 | @@ -13,6 +13,8 @@ | |||
642 | 13 | from twisted.internet.threads import deferToThread | 13 | from twisted.internet.threads import deferToThread |
643 | 14 | from twisted.internet import defer | 14 | from twisted.internet import defer |
644 | 15 | 15 | ||
645 | 16 | from neo4jrestclient.exceptions import TransactionException | ||
646 | 17 | |||
647 | 16 | import logging | 18 | import logging |
648 | 17 | import os | 19 | import os |
649 | 18 | import json | 20 | import json |
650 | @@ -24,6 +26,8 @@ | |||
651 | 24 | import tempfile | 26 | import tempfile |
652 | 25 | import subprocess | 27 | import subprocess |
653 | 26 | 28 | ||
654 | 29 | from datetime import datetime | ||
655 | 30 | |||
656 | 27 | from cgi import escape # deprecated in Python 3 in favour of html.escape, but we're stuck on Python 2 | 31 | from cgi import escape # deprecated in Python 3 in favour of html.escape, but we're stuck on Python 2 |
657 | 28 | 32 | ||
658 | 29 | # global SextantConnection object which deals with the port forwarding | 33 | # global SextantConnection object which deals with the port forwarding |
659 | @@ -174,13 +178,15 @@ | |||
660 | 174 | # if we are okay here we have a valid query with all required arguments | 178 | # if we are okay here we have a valid query with all required arguments |
661 | 175 | if res_code is RESPONSE_CODE_OK: | 179 | if res_code is RESPONSE_CODE_OK: |
662 | 176 | try: | 180 | try: |
663 | 181 | print('running query {}'.format(datetime.now())) | ||
664 | 177 | program = yield defer_to_thread_with_timeout(render_timeout, fn, | 182 | program = yield defer_to_thread_with_timeout(render_timeout, fn, |
665 | 178 | name, *req_args) | 183 | name, *req_args) |
667 | 179 | except defer.CancelledError: | 184 | print('\tdone {}'.format(datetime.now())) |
668 | 185 | except Exception as e: | ||
669 | 180 | # the timeout has fired and cancelled the request | 186 | # the timeout has fired and cancelled the request |
670 | 181 | res_code = RESPONSE_CODE_BAD_REQUEST | 187 | res_code = RESPONSE_CODE_BAD_REQUEST |
673 | 182 | res_fmt = "The request timed out after {} seconds." | 188 | res_msg = "{}".format(e) |
674 | 183 | res_msg = res_fmt.format(render_timeout) | 189 | print('\tfailed {}'.format(datetime.now())) |
675 | 184 | 190 | ||
676 | 185 | if res_code is RESPONSE_CODE_OK: | 191 | if res_code is RESPONSE_CODE_OK: |
677 | 186 | # we have received a response to our request | 192 | # we have received a response to our request |
678 | @@ -201,10 +207,12 @@ | |||
679 | 201 | suppress_common = suppress_common_arg in ('null', 'true') | 207 | suppress_common = suppress_common_arg in ('null', 'true') |
680 | 202 | 208 | ||
681 | 203 | # we have a non-empty return - render it | 209 | # we have a non-empty return - render it |
682 | 210 | print('getting plot {}'.format(datetime.now())) | ||
683 | 204 | res_msg = yield deferToThread(self.get_plot, program, | 211 | res_msg = yield deferToThread(self.get_plot, program, |
684 | 205 | suppress_common, | 212 | suppress_common, |
685 | 206 | remove_self_calls=False) | 213 | remove_self_calls=False) |
686 | 207 | request.setHeader('content-type', 'image/svg+xml') | 214 | request.setHeader('content-type', 'image/svg+xml') |
687 | 215 | print('\tdone {}'.format(datetime.now())) | ||
688 | 208 | 216 | ||
689 | 209 | request.setResponseCode(res_code) | 217 | request.setResponseCode(res_code) |
690 | 210 | request.write(res_msg) | 218 | request.write(res_msg) |
691 | @@ -229,6 +237,7 @@ | |||
692 | 229 | max_funcs = AUTOCOMPLETE_NAMES_LIMIT + 1 | 237 | max_funcs = AUTOCOMPLETE_NAMES_LIMIT + 1 |
693 | 230 | programs = CONNECTION.programs_with_metadata() | 238 | programs = CONNECTION.programs_with_metadata() |
694 | 231 | result = CONNECTION.get_function_names(program_name, search, max_funcs) | 239 | result = CONNECTION.get_function_names(program_name, search, max_funcs) |
695 | 240 | print(search, len(result)) | ||
696 | 232 | return result if len(result) < max_funcs else set() | 241 | return result if len(result) < max_funcs else set() |
697 | 233 | 242 | ||
698 | 234 | 243 |
The prerequisite lp:~ben-hutchings/ensoft-sextant/autocomplete-fix has not yet been merged into lp:ensoft-sextant.