Merge lp:~ensoft-opensource/ensoft-sextant/trunk into lp:ensoft-sextant
- trunk
- Merge into whiteline
Proposed by
James
Status: | Merged |
---|---|
Approved by: | Martin Morrison |
Approved revision: | 178 |
Merged at revision: | 2 |
Proposed branch: | lp:~ensoft-opensource/ensoft-sextant/trunk |
Merge into: | lp:ensoft-sextant |
Diff against target: |
3108 lines (+2792/-161) 27 files modified
LICENSE.txt (+27/-0) MANIFEST.in (+3/-0) Parser/parser.py (+0/-161) bin/sextant (+8/-0) bin/sextant.bat (+3/-0) doc/Program_upload_docs.mkd (+9/-0) doc/Web_server.mkd (+20/-0) doc/wiki/Reference (+45/-0) etc/sextant.conf (+7/-0) resources/web/index.html (+45/-0) resources/web/interface.html (+103/-0) resources/web/queryjavascript.js (+201/-0) resources/web/style_sheet.css (+36/-0) setup.cfg (+2/-0) setup.py (+25/-0) src/sextant/__init__.py (+21/-0) src/sextant/__main__.py (+220/-0) src/sextant/db_api.py (+610/-0) src/sextant/errors.py (+20/-0) src/sextant/export.py (+152/-0) src/sextant/objdump_parser.py (+272/-0) src/sextant/pyinput.py (+180/-0) src/sextant/query.py (+113/-0) src/sextant/tests.py (+261/-0) src/sextant/update_db.py (+78/-0) src/sextant/web/__init__.py (+8/-0) src/sextant/web/server.py (+323/-0) |
To merge this branch: | bzr merge lp:~ensoft-opensource/ensoft-sextant/trunk |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Phil Connell | Approve | ||
Ensoft Patch Lander | Pending | ||
Review via email: mp+230964@code.launchpad.net |
Commit message
setup.py no uploads config and web file to the correct location for the pypi installation.
Description of the change
setup.py no uploads config and web file to the correct location for the pypi installation.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'LICENSE.txt' | |||
2 | --- LICENSE.txt 1970-01-01 00:00:00 +0000 | |||
3 | +++ LICENSE.txt 2014-08-15 12:04:08 +0000 | |||
4 | @@ -0,0 +1,27 @@ | |||
5 | 1 | Copyright (c) 2014, Ensoft Ltd. | ||
6 | 2 | All rights reserved. | ||
7 | 3 | |||
8 | 4 | Redistribution and use in source and binary forms, with or without | ||
9 | 5 | modification, are permitted provided that the following conditions are met: | ||
10 | 6 | |||
11 | 7 | 1. Redistributions of source code must retain the above copyright notice, this | ||
12 | 8 | list of conditions and the following disclaimer. | ||
13 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
14 | 10 | this list of conditions and the following disclaimer in the documentation | ||
15 | 11 | and/or other materials provided with the distribution. | ||
16 | 12 | |||
17 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
18 | 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
19 | 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
20 | 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
21 | 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
22 | 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
23 | 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
24 | 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
25 | 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
26 | 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
27 | 23 | |||
28 | 24 | The views and conclusions contained in the software and documentation are those | ||
29 | 25 | of the authors and should not be interpreted as representing official policies, | ||
30 | 26 | either expressed or implied, of the FreeBSD Project. | ||
31 | 27 | |||
32 | 0 | 28 | ||
33 | === added file 'MANIFEST.in' | |||
34 | --- MANIFEST.in 1970-01-01 00:00:00 +0000 | |||
35 | +++ MANIFEST.in 2014-08-15 12:04:08 +0000 | |||
36 | @@ -0,0 +1,3 @@ | |||
37 | 1 | include *.py | ||
38 | 2 | recursive-include resources *.html *.js *.css *.gif | ||
39 | 3 | |||
40 | 0 | 4 | ||
41 | === removed directory 'Parser' | |||
42 | === removed file 'Parser/parser.py' | |||
43 | --- Parser/parser.py 2014-07-03 12:28:28 +0000 | |||
44 | +++ Parser/parser.py 1970-01-01 00:00:00 +0000 | |||
45 | @@ -1,161 +0,0 @@ | |||
46 | 1 | __author__ = 'patrickas' | ||
47 | 2 | |||
48 | 3 | import re | ||
49 | 4 | import argparse | ||
50 | 5 | |||
51 | 6 | class parsed_object(): | ||
52 | 7 | """This object has a name (which is the verbatim name like '<__libc_start_main@plt>'), a position (which | ||
53 | 8 | is the virtual memory location in hex, like '08048320', extracted from the dump), and a canonical_position | ||
54 | 9 | (which is the virtual memory location in hex but stripped of leading 0s, so it should be a unique id). | ||
55 | 10 | It also has a list what_do_i_call of parsed_objects it calls using the assembly keyword 'call'. | ||
56 | 11 | It has a list original_code of its assembler code, too, in case it's useful.""" | ||
57 | 12 | |||
58 | 13 | @staticmethod | ||
59 | 14 | def get_canonical_position(position): | ||
60 | 15 | return position.lstrip('0') | ||
61 | 16 | |||
62 | 17 | def __eq__(self, other): | ||
63 | 18 | return self.canonical_position == other.canonical_position and self.name == other.name | ||
64 | 19 | |||
65 | 20 | def __init__(self, input_lines, assembler_section = ''): | ||
66 | 21 | """Creates a new parsed_object given the relevant definition-lines from objdump -S. | ||
67 | 22 | A sample first definition-line is '08048300 <__gmon_start__@plt>:\n' but this function | ||
68 | 23 | expects to see the entire definition eg | ||
69 | 24 | |||
70 | 25 | 080482f0 <puts@plt>: | ||
71 | 26 | 80482f0: ff 25 00 a0 04 08 jmp *0x804a000 | ||
72 | 27 | 80482f6: 68 00 00 00 00 push $0x0 | ||
73 | 28 | 80482fb: e9 e0 ff ff ff jmp 80482e0 <_init+0x30> | ||
74 | 29 | |||
75 | 30 | We also might expect assembler_section, which is for instance '.init' in 'Disassembly of section .init:' | ||
76 | 31 | """ | ||
77 | 32 | self.name = re.search(r'<.+>', input_lines[0]).group(0) | ||
78 | 33 | self.position = re.search(r'^[0-9a-f]+', input_lines[0]).group(0) | ||
79 | 34 | self.canonical_position = parsed_object.get_canonical_position(self.position) | ||
80 | 35 | self.assembler_section = assembler_section | ||
81 | 36 | self.original_code = input_lines[1:] | ||
82 | 37 | |||
83 | 38 | #todo: work out what this node calls, and store it in what_do_i_call | ||
84 | 39 | lines_where_i_call = [line for line in input_lines if re.search(r'\tcall [0-9a-f]+ <.+>\n', line)] | ||
85 | 40 | |||
86 | 41 | self.what_do_i_call = [] | ||
87 | 42 | for line in lines_where_i_call: | ||
88 | 43 | called = (re.search(r'\tcall [0-9a-f]+ <.+>\n', line).group(0))[8:] | ||
89 | 44 | address, name = called.split(' ') | ||
90 | 45 | self.what_do_i_call.append((address, name.rstrip('\n'))) | ||
91 | 46 | |||
92 | 47 | |||
93 | 48 | |||
94 | 49 | def __str__(self): | ||
95 | 50 | return 'Memory address ' + self.position + ' with name ' + self.name + ' in section ' + str(self.assembler_section) | ||
96 | 51 | |||
97 | 52 | def __repr__(self): | ||
98 | 53 | out_str = 'Disassembly of section ' + self.assembler_section + ':\n\n' + self.position + ' ' + self.name + ':\n' | ||
99 | 54 | return out_str + '\n'.join([' ' + line for line in self.original_code]) | ||
100 | 55 | |||
101 | 56 | class Parser: | ||
102 | 57 | # Class to manipulate the output of objdump | ||
103 | 58 | |||
104 | 59 | def __init__(self, input_file_location): | ||
105 | 60 | """Creates a new Parser, given an input file path. That path should be an output from objdump -S.""" | ||
106 | 61 | file = open(input_file_location, 'r') | ||
107 | 62 | self.source_string_list = [line for line in file] | ||
108 | 63 | file.close() | ||
109 | 64 | self.parsed_objects = [] | ||
110 | 65 | |||
111 | 66 | def create_objects(self): | ||
112 | 67 | """ Go through the source_string_list, getting object names (like <main>) along with the corresponding | ||
113 | 68 | definitions, and put them into parsed_objects """ | ||
114 | 69 | |||
115 | 70 | parsed_objects = [] | ||
116 | 71 | current_object = [] | ||
117 | 72 | current_section = '' | ||
118 | 73 | for line in self.source_string_list[4:]: # we bodge, since the file starts with a little bit of guff | ||
119 | 74 | if re.match(r'[0-9a-f]+ <.+>:\n', line): | ||
120 | 75 | # we are a starting line | ||
121 | 76 | current_object = [line] | ||
122 | 77 | elif re.match(r'Disassembly of section', line): | ||
123 | 78 | current_section = re.search(r'section .+:\n', line).group(0).lstrip('section ').rstrip(':\n') | ||
124 | 79 | current_object = [] | ||
125 | 80 | elif line == '\n': | ||
126 | 81 | #we now need to stop parsing the current block, and store it | ||
127 | 82 | if len(current_object) > 0: | ||
128 | 83 | parsed_objects.append(parsed_object(current_object, current_section)) | ||
129 | 84 | else: | ||
130 | 85 | current_object.append(line) | ||
131 | 86 | |||
132 | 87 | # now we should be done. We assumed that blocks begin with r'[0-9a-f]+ <.+>:\n' and end with a newline. | ||
133 | 88 | #clear duplicates: | ||
134 | 89 | |||
135 | 90 | self.parsed_objects = [] | ||
136 | 91 | for obj in parsed_objects: | ||
137 | 92 | if obj not in self.parsed_objects: | ||
138 | 93 | self.parsed_objects.append(obj) | ||
139 | 94 | |||
140 | 95 | # by this point, each object contains a self.what_do_i_call which is a list of tuples ('address', 'name'). | ||
141 | 96 | |||
142 | 97 | def object_lookup(self, object_name = '', object_address = ''): | ||
143 | 98 | """Returns the object with name object_name or address object_address (at least one must be given). | ||
144 | 99 | If objects with the given name or address | ||
145 | 100 | are not found, returns None.""" | ||
146 | 101 | |||
147 | 102 | if object_name == '' and object_address == '': | ||
148 | 103 | return None | ||
149 | 104 | |||
150 | 105 | trial_obj = self.parsed_objects | ||
151 | 106 | |||
152 | 107 | if object_name != '': | ||
153 | 108 | trial_obj = [obj for obj in trial_obj if obj.name == object_name] | ||
154 | 109 | |||
155 | 110 | if object_address != '': | ||
156 | 111 | trial_obj = [obj for obj in trial_obj if obj.canonical_position == parsed_object.get_canonical_position(object_address)] | ||
157 | 112 | |||
158 | 113 | if len(trial_obj) == 0: | ||
159 | 114 | return None | ||
160 | 115 | |||
161 | 116 | return trial_obj | ||
162 | 117 | |||
163 | 118 | def link_objects(self): | ||
164 | 119 | """Goes through the objects which have already been imported, making their self.what_do_i_call into a list of | ||
165 | 120 | objects, rather than a list of (address, name)""" | ||
166 | 121 | #todo: implement this | ||
167 | 122 | |||
168 | 123 | for obj in self.parsed_objects: | ||
169 | 124 | output_list = [] | ||
170 | 125 | for called_thing in obj.what_do_i_call: | ||
171 | 126 | ref = called_thing[0] | ||
172 | 127 | if self.object_lookup(object_address=ref) is None: | ||
173 | 128 | #here, we have an object which is not found in our object_lookup | ||
174 | 129 | pass | ||
175 | 130 | else: | ||
176 | 131 | #called_thing looks like ('address', 'name'); we need to pass this list one by one into object_lookup | ||
177 | 132 | for looked_up in self.object_lookup(object_address=ref): | ||
178 | 133 | output_list.append(looked_up) | ||
179 | 134 | obj.what_do_i_call = output_list | ||
180 | 135 | |||
181 | 136 | def main(): | ||
182 | 137 | |||
183 | 138 | |||
184 | 139 | p = Parser(r"C:\Users\patrickas\Desktop\Objdump_parser\objdump_samples\objdump_libcopp.txt") #command line input of this, or accept a .o file (to run objdump against) | ||
185 | 140 | p.create_objects() | ||
186 | 141 | |||
187 | 142 | print('Functions found:') | ||
188 | 143 | |||
189 | 144 | for obj in p.parsed_objects: | ||
190 | 145 | print(str(obj)) | ||
191 | 146 | |||
192 | 147 | p.link_objects() | ||
193 | 148 | |||
194 | 149 | func_to_investigate = '<copp_sampler_et_ht_lookup>' #do this for all functions | ||
195 | 150 | |||
196 | 151 | print('Investigation of {}:'.format(func_to_investigate)) | ||
197 | 152 | |||
198 | 153 | #we can investigate a particular object: | ||
199 | 154 | main_func = [obj for obj in p.parsed_objects if obj.name == func_to_investigate][0] | ||
200 | 155 | print('{} calls '.format(func_to_investigate),end='') | ||
201 | 156 | print([str(f) for f in main_func.what_do_i_call]) | ||
202 | 157 | |||
203 | 158 | print('Ended investigation') | ||
204 | 159 | |||
205 | 160 | if __name__ == "__main__": | ||
206 | 161 | main() | ||
207 | 162 | \ No newline at end of file | 0 | \ No newline at end of file |
208 | 163 | 1 | ||
209 | === added directory 'bin' | |||
210 | === added file 'bin/sextant' | |||
211 | --- bin/sextant 1970-01-01 00:00:00 +0000 | |||
212 | +++ bin/sextant 2014-08-15 12:04:08 +0000 | |||
213 | @@ -0,0 +1,8 @@ | |||
214 | 1 | #!/bin/bash | ||
215 | 2 | |||
216 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | ||
217 | 4 | |||
218 | 5 | export PYTHONPATH=${PYTHONPATH}:${DIR}/../src | ||
219 | 6 | |||
220 | 7 | python -m sextant $@ | ||
221 | 8 | |||
222 | 0 | 9 | ||
223 | === added file 'bin/sextant.bat' | |||
224 | --- bin/sextant.bat 1970-01-01 00:00:00 +0000 | |||
225 | +++ bin/sextant.bat 2014-08-15 12:04:08 +0000 | |||
226 | @@ -0,0 +1,3 @@ | |||
227 | 1 | set DIR=%~dp0..\src\ | ||
228 | 2 | |||
229 | 3 | cmd /C "set PYTHONPATH=%DIR% && c:\python27\python -m sextant %*" | ||
230 | 0 | 4 | ||
231 | === added directory 'doc' | |||
232 | === added file 'doc/Instructions For yED use.docx' | |||
233 | 1 | Binary files doc/Instructions For yED use.docx 1970-01-01 00:00:00 +0000 and doc/Instructions For yED use.docx 2014-08-15 12:04:08 +0000 differ | 5 | Binary files doc/Instructions For yED use.docx 1970-01-01 00:00:00 +0000 and doc/Instructions For yED use.docx 2014-08-15 12:04:08 +0000 differ |
234 | === added file 'doc/Program_upload_docs.mkd' | |||
235 | --- doc/Program_upload_docs.mkd 1970-01-01 00:00:00 +0000 | |||
236 | +++ doc/Program_upload_docs.mkd 2014-08-15 12:04:08 +0000 | |||
237 | @@ -0,0 +1,9 @@ | |||
238 | 1 | # Function pointers | ||
239 | 2 | |||
240 | 3 | We treat function pointers as functions in their own right, but we rename them to `_._function_pointer_n` where `n` is a numerical identifier (unique to each function pointer call). When ProgramConverter is used to produce graphical output for graphs with function pointers in, they are displayed as yellow nodes with name `(function pointer)`. | ||
241 | 4 | |||
242 | 5 | Optionally, the flag `--ignore-function-pointers` can be provided to `program_upload.py` to cause the parser to pretend function pointer calls simply do not exist. | ||
243 | 6 | |||
244 | 7 | # Query parameters | ||
245 | 8 | We take the mandatory `--input-file`, a filepath for a program file whose call graph we wish to upload to the Neo4J server. Optionally we can specify that it is an ``--object-file` to have `objdump` run against it; otherwise, we assume the input was a text file consisting of `objdump`'s output. | ||
246 | 9 | Optional (if specified in config file) `--remote-neo4j`, the location of the Neo4J server (eg. `http://localhost:7474`). | ||
247 | 0 | \ No newline at end of file | 10 | \ No newline at end of file |
248 | 1 | 11 | ||
249 | === added file 'doc/Web_server.mkd' | |||
250 | --- doc/Web_server.mkd 1970-01-01 00:00:00 +0000 | |||
251 | +++ doc/Web_server.mkd 2014-08-15 12:04:08 +0000 | |||
252 | @@ -0,0 +1,20 @@ | |||
253 | 1 | The server is running on Twisted. It has a static directory (`/trunk/sextant/web/public_html` in the repository) hosted at `/hello_world` for the moment. It also has `/database_properties` and `/output_graph.svg`. This page documents (so far) the dynamically generated pages. | ||
254 | 2 | |||
255 | 3 | # database_properties | ||
256 | 4 | This page is designed to be roughly equivalent to the Python script `/graph_properties.py`. It takes arguments `query` of 'functions' or 'properties' and optionally `program_name` (which is ignored unless 'query=functions' was specified). Its return content-type is `text/plain`. The page returns a list of function names as: | ||
257 | 5 | |||
258 | 6 | ["func1", "func2", ... , "funcn"] | ||
259 | 7 | |||
260 | 8 | if `query=functions` was specified, where the `funci` are the function names in the database under program-name `program_name`. It returns `None` if there was no program with the given name, and `[]` if there were no functions in the existing program with the given name. | ||
261 | 9 | |||
262 | 10 | If instead `query=programs` was specified, we return: | ||
263 | 11 | |||
264 | 12 | ["prog1", ... , "progn"] | ||
265 | 13 | |||
266 | 14 | where the `progi` are the program names stored in the database. If there are no programs in the database, we return `[]`. | ||
267 | 15 | |||
268 | 16 | # output_graph.svg | ||
269 | 17 | This page is roughly equivalent to `/query_graph.py`. It takes arguments `program_name`, `query`. If the `program_name` was not found in the database, raises a 404 Not Found error. The `query` must be `whole_program`, `functions_calling` or `functions_called_by`; if it is not found, we use `whole_program` as a default. | ||
270 | 18 | If `query` is `functions_calling` or `functions_called_by`, we require a `func1` parameter, as the name of the function of interest. | ||
271 | 19 | |||
272 | 20 | The page returns a GraphViz-generated file of the query run against the specified function. Its content-type is `image/svg+xml`. | ||
273 | 0 | \ No newline at end of file | 21 | \ No newline at end of file |
274 | 1 | 22 | ||
275 | === added directory 'doc/wiki' | |||
276 | === added file 'doc/wiki/Reference' | |||
277 | --- doc/wiki/Reference 1970-01-01 00:00:00 +0000 | |||
278 | +++ doc/wiki/Reference 2014-08-15 12:04:08 +0000 | |||
279 | @@ -0,0 +1,45 @@ | |||
280 | 1 | |||
281 | 2 | = Sextant Notes = | ||
282 | 3 | == Configuration == | ||
283 | 4 | In the config file you should specify: | ||
284 | 5 | * The url under which Neo4j is running, | ||
285 | 6 | * The port on which the web interface will run, | ||
286 | 7 | * Your definition of what is a common function (number of functions calling that function). | ||
287 | 8 | An example file is provided in the etc folder. | ||
288 | 9 | If your instance of Neo4j is local, please start running the server before uploading a program or performing queries. | ||
289 | 10 | == File Management == | ||
290 | 11 | No more than one program can have the same name in the database. | ||
291 | 12 | === Uploading a File === | ||
292 | 13 | set-file-name and not-object-file are optional. | ||
293 | 14 | {{{ | ||
294 | 15 | sextant add_program --input-file <name of file being input> --set-file-name <name file to be stored under in database> --not-object-file <True or False (default is False; pass through Objdump)> | ||
295 | 16 | }}} | ||
296 | 17 | === Deleting File === | ||
297 | 18 | It is good practice to delete files from the server since having many graphs in the database can lead to negative effects on performance. | ||
298 | 19 | {{{ | ||
299 | 20 | sextant delete_program --program name <program name to be deleted as stored in database> | ||
300 | 21 | }}} | ||
301 | 22 | == Queries == | ||
302 | 23 | === Command Line === | ||
303 | 24 | Command line queries produce a text output, either as a list or in GraphML which can be opened in yED. | ||
304 | 25 | All queries take the form. | ||
305 | 26 | {{{ | ||
306 | 27 | sextant query --program-name <program name> --query <query name> --funcs <One or two functions as the arguments of the query> --suppress-common <True or False (default is False)> | ||
307 | 28 | }}} | ||
308 | 29 | Here common sense prevails. "return-all-program-names" doesn't have a "--program-name" argument or "--funcs" or "--suppress-common". Similarly whole-graph doesn't take any "--funcs". "--suppress-common" is always optional and can only be used for a GraphML output. | ||
309 | 30 | The options for queries are: | ||
310 | 31 | * functions-calling, | ||
311 | 32 | * functions-called-by, | ||
312 | 33 | * calls-between, | ||
313 | 34 | * whole-graph, | ||
314 | 35 | * shortest-path, | ||
315 | 36 | * return-all-program-names, | ||
316 | 37 | * return-all-function-names. | ||
317 | 38 | For help with queries from the command line and the arguments type: | ||
318 | 39 | {{{ | ||
319 | 40 | sextant query -h | ||
320 | 41 | }}} | ||
321 | 42 | === Web === | ||
322 | 43 | To run the web server type | ||
323 | 44 | {{{ sextant web }}} | ||
324 | 45 | into the command line; then navigate a web browser to the port specified. | ||
325 | 0 | \ No newline at end of file | 46 | \ No newline at end of file |
326 | 1 | 47 | ||
327 | === added directory 'etc' | |||
328 | === added file 'etc/sextant.conf' | |||
329 | --- etc/sextant.conf 1970-01-01 00:00:00 +0000 | |||
330 | +++ etc/sextant.conf 2014-08-15 12:04:08 +0000 | |||
331 | @@ -0,0 +1,7 @@ | |||
332 | 1 | [Neo4j] | ||
333 | 2 | # URL of the Neo4J server | ||
334 | 3 | remote_neo4j = http://localhost:7474 | ||
335 | 4 | # port on which to serve Sextant Web | ||
336 | 5 | port = 2905 | ||
337 | 6 | # number of calls incoming before we consider a function to be 'common' | ||
338 | 7 | common_function_calls = 10 | ||
339 | 0 | 8 | ||
340 | === added directory 'resources' | |||
341 | === added directory 'resources/web' | |||
342 | === added file 'resources/web/index.html' | |||
343 | --- resources/web/index.html 1970-01-01 00:00:00 +0000 | |||
344 | +++ resources/web/index.html 2014-08-15 12:04:08 +0000 | |||
345 | @@ -0,0 +1,45 @@ | |||
346 | 1 | <!-- | ||
347 | 2 | Sextant | ||
348 | 3 | Copyright 2014, Ensoft Ltd. | ||
349 | 4 | Author: Patrick Stevens | ||
350 | 5 | |||
351 | 6 | Home screen for Sextant--> | ||
352 | 7 | |||
353 | 8 | <!DOCTYPE html> | ||
354 | 9 | <html> | ||
355 | 10 | <head> | ||
356 | 11 | <meta charset="utf-8"/> | ||
357 | 12 | <link rel="stylesheet" type="text/css" href="style_sheet.css"/> | ||
358 | 13 | <title>Sextant</title> | ||
359 | 14 | </head> | ||
360 | 15 | <body> | ||
361 | 16 | <table> | ||
362 | 17 | <tr> | ||
363 | 18 | <td> | ||
364 | 19 | <h1>You have reached the portal to Sextant.</h1> | ||
365 | 20 | </td> | ||
366 | 21 | <td> | ||
367 | 22 | </td> | ||
368 | 23 | </tr> | ||
369 | 24 | <tr> | ||
370 | 25 | <td> | ||
371 | 26 | <!--Sextant image--> | ||
372 | 27 | <img src="sextant.gif" alt = "sextant"> | ||
373 | 28 | </td> | ||
374 | 29 | <td> | ||
375 | 30 | <!--link to the Query page--> | ||
376 | 31 | <form action='/interface.html' method="get"> | ||
377 | 32 | <input type="submit" value="Query Page" class="button2" | ||
378 | 33 | name="link_query" id="form1_submit" /> | ||
379 | 34 | </form> | ||
380 | 35 | <!--link to Database Properties page--> | ||
381 | 36 | <form action='/database_properties' method="get"> | ||
382 | 37 | <input type="submit" value="Database Properties" class="button2" | ||
383 | 38 | name="link_properties" id="form2_submit" /> | ||
384 | 39 | </form> | ||
385 | 40 | </td> | ||
386 | 41 | </tr> | ||
387 | 42 | </table> | ||
388 | 43 | |||
389 | 44 | </body> | ||
390 | 45 | </html> | ||
391 | 0 | 46 | ||
392 | === added file 'resources/web/interface.html' | |||
393 | --- resources/web/interface.html 1970-01-01 00:00:00 +0000 | |||
394 | +++ resources/web/interface.html 2014-08-15 12:04:08 +0000 | |||
395 | @@ -0,0 +1,103 @@ | |||
396 | 1 | <!-- | ||
397 | 2 | Sextant | ||
398 | 3 | Copyright 2014, Ensoft Ltd. | ||
399 | 4 | Author: James Harkin | ||
400 | 5 | |||
401 | 6 | Sextant web interface for Queries --> | ||
402 | 7 | |||
403 | 8 | <!DOCTYPE html> | ||
404 | 9 | <html> | ||
405 | 10 | <head> | ||
406 | 11 | <meta charset="utf-8"/> | ||
407 | 12 | <link rel="stylesheet" type="text/css" href="style_sheet.css"/> | ||
408 | 13 | <title> Webinterface test </title> | ||
409 | 14 | </head> | ||
410 | 15 | |||
411 | 16 | <body onload="get_names_for_autocomplete('programs')"> | ||
412 | 17 | <!-- Create a table with a border to hold the input file query name | ||
413 | 18 | and arguments. table remains fixed while page scrolls--> | ||
414 | 19 | <table style="background-color:#FFFFFF; width: 100%; | ||
415 | 20 | border:1px solid black; position: fixed;"> | ||
416 | 21 | <tr style="height:20px"> | ||
417 | 22 | <td style="width:250"> | ||
418 | 23 | <h2>Input file</h2> | ||
419 | 24 | </td> | ||
420 | 25 | <td style="width:350"> | ||
421 | 26 | <h2>Query</h2> | ||
422 | 27 | </td> | ||
423 | 28 | <td style="width:250"> | ||
424 | 29 | <h2>Arguments</h2> | ||
425 | 30 | </td> | ||
426 | 31 | <td> | ||
427 | 32 | <form> | ||
428 | 33 | <!--checkbox used to supress common nodes--> | ||
429 | 34 | <label> | ||
430 | 35 | <input type="checkbox" id="suppress_common" value="True" /> | ||
431 | 36 | Suppress common functions? | ||
432 | 37 | </label> | ||
433 | 38 | </form> | ||
434 | 39 | </td> | ||
435 | 40 | </tr> | ||
436 | 41 | <tr style="height:100px"> | ||
437 | 42 | <td> | ||
438 | 43 | <!-- Autocomplete textbox for program names; populated on upload--> | ||
439 | 44 | <input list="program_names" id="program_name" | ||
440 | 45 | onblur="get_names_for_autocomplete('funcs')" style="size:20"> | ||
441 | 46 | <datalist id="program_names"> | ||
442 | 47 | </datalist> | ||
443 | 48 | </td> | ||
444 | 49 | <td> | ||
445 | 50 | <!-- Drop-down box to hold choice of queries--> | ||
446 | 51 | <form name="drop_list_1"> | ||
447 | 52 | <SELECT id="query_list" onchange="display_when();"> | ||
448 | 53 | <option value="whole_program">Whole graph</option> | ||
449 | 54 | <option value="functions_calling"> | ||
450 | 55 | All functions calling specific function</option> | ||
451 | 56 | <option value="functions_called_by"> | ||
452 | 57 | All functions called by a specific function</option> | ||
453 | 58 | <option value="call_paths"> | ||
454 | 59 | All function call paths between two functions</option> | ||
455 | 60 | <option value="shortest_path"> | ||
456 | 61 | Shortest path between two functions</option> | ||
457 | 62 | <option value="function_names"> | ||
458 | 63 | All function names</option> | ||
459 | 64 | </SELECT> | ||
460 | 65 | </form> | ||
461 | 66 | <p> | ||
462 | 67 | query selected : <input type="text" id="query" | ||
463 | 68 | value="Whole graph" size="20"> | ||
464 | 69 | </p> | ||
465 | 70 | </td> | ||
466 | 71 | <td> | ||
467 | 72 | <!-- Autocomplete text box for argument | ||
468 | 73 | 1 Is only visible for relevant queries--> | ||
469 | 74 | <div id="argument_1">No arguments required.</div> | ||
470 | 75 | <input list="function_names" id="function_1" style="size:20; visibility:hidden"> | ||
471 | 76 | <!--list to populate arguments. Updates when program name is specified--> | ||
472 | 77 | <datalist id="function_names"> | ||
473 | 78 | </datalist> | ||
474 | 79 | <!-- Autocomplete text box for argument | ||
475 | 80 | 2 Is only visible for relevant queries--> | ||
476 | 81 | <div id="argument_2"></div> | ||
477 | 82 | <input list="function_names" id="function_2" style="size:20; visibility:hidden"> | ||
478 | 83 | |||
479 | 84 | </td> | ||
480 | 85 | <td> | ||
481 | 86 | |||
482 | 87 | <!--execute button; submits query--> | ||
483 | 88 | <br><input type="submit" value="Execute
Query" class="button" | ||
484 | 89 | onclick="execute_query()"><br> | ||
485 | 90 | </td> | ||
486 | 91 | </tr> | ||
487 | 92 | </table> | ||
488 | 93 | <!-- Paragraph for text output e.g. list of funtion names--> | ||
489 | 94 | <p id="function_names_output" class="pos_functions" style="size:20; visibility:hidden"></p> | ||
490 | 95 | |||
491 | 96 | <!-- Output for image file--> | ||
492 | 97 | <img id=output_image src="sextant.jpg" alt="Execute query to display graph." | ||
493 | 98 | class="pos_img" | ||
494 | 99 | style="align: bottom; font-style: italic; color: #C0C0C0; font-size: 15px;"> | ||
495 | 100 | |||
496 | 101 | <script src="queryjavascript.js"></script> | ||
497 | 102 | </body> | ||
498 | 103 | </html> | ||
499 | 0 | 104 | ||
500 | === added file 'resources/web/queryjavascript.js' | |||
501 | --- resources/web/queryjavascript.js 1970-01-01 00:00:00 +0000 | |||
502 | +++ resources/web/queryjavascript.js 2014-08-15 12:04:08 +0000 | |||
503 | @@ -0,0 +1,201 @@ | |||
504 | 1 | // Sextant | ||
505 | 2 | // Copyright 2014, Ensoft Ltd. | ||
506 | 3 | // Author: James Harkin, Patrick Stevens | ||
507 | 4 | // | ||
508 | 5 | //Runs query and "GET"s either program names uploaded on the | ||
509 | 6 | //server or function names from a specific program. | ||
510 | 7 | |||
511 | 8 | |||
512 | 9 | function get_names_for_autocomplete(info_needed){ | ||
513 | 10 | //Function queries to database to create a list | ||
514 | 11 | //which is used to populate the auto-complete text boxes. | ||
515 | 12 | var xmlhttp = new XMLHttpRequest(); | ||
516 | 13 | xmlhttp.onreadystatechange = function(){ | ||
517 | 14 | if (xmlhttp.status = 200){ | ||
518 | 15 | var values_list = xmlhttp.responseText; | ||
519 | 16 | console.log(values_list) | ||
520 | 17 | values_list = JSON.parse(values_list); | ||
521 | 18 | if (info_needed =='programs'){ | ||
522 | 19 | //We need to populate the program names list | ||
523 | 20 | add_options("program_names", values_list); | ||
524 | 21 | } | ||
525 | 22 | if (info_needed =='funcs'){ | ||
526 | 23 | //We need to populate the functions list for the arguments | ||
527 | 24 | add_options("function_names", values_list); | ||
528 | 25 | } | ||
529 | 26 | } | ||
530 | 27 | } | ||
531 | 28 | if (info_needed == 'programs'){ | ||
532 | 29 | var string = "/database_properties?query=" + info_needed + "&program_name="; | ||
533 | 30 | } | ||
534 | 31 | else{ | ||
535 | 32 | var string = "/database_properties?query=" + "functions" + | ||
536 | 33 | "&program_name=" + document.getElementById("program_name").value; | ||
537 | 34 | if (info_needed == 'programs'){ | ||
538 | 35 | var string = "/database_properties?query=" + | ||
539 | 36 | info_needed + "&program_name=" + prog_name; | ||
540 | 37 | } | ||
541 | 38 | //"GET" information from the specified url (string) | ||
542 | 39 | xmlhttp.open("GET", string, true); | ||
543 | 40 | xmlhttp.send(); | ||
544 | 41 | } | ||
545 | 42 | xmlhttp.open("GET", string, true); | ||
546 | 43 | xmlhttp.send(); | ||
547 | 44 | } | ||
548 | 45 | |||
549 | 46 | |||
550 | 47 | function add_options(selectedlist, values_list){ | ||
551 | 48 | //Adds all the options obtained from the list of program | ||
552 | 49 | //names or function names to an auto complete drop-down box | ||
553 | 50 | var options = '' | ||
554 | 51 | if (values_list.length == 1 || values_list.length ==0){ | ||
555 | 52 | options += '<option value="'+values_list+'"/>'; | ||
556 | 53 | } | ||
557 | 54 | else{ | ||
558 | 55 | for (var i=0; i < values_list.length;++i){ | ||
559 | 56 | options += '<option value="'+values_list[i]+'"/>'; | ||
560 | 57 | } | ||
561 | 58 | } | ||
562 | 59 | document.getElementById(selectedlist).innerHTML = options; | ||
563 | 60 | } | ||
564 | 61 | |||
565 | 62 | |||
566 | 63 | function display_when(){ | ||
567 | 64 | //For each query specifies when auto-complete text boxes should be made | ||
568 | 65 | //visible or invisible and makes them read only | ||
569 | 66 | var query_list = document.getElementById("query_list"); | ||
570 | 67 | document.getElementById("query").value = | ||
571 | 68 | query_list.options[query_list.selectedIndex].text; | ||
572 | 69 | var no_functions = new Array(); | ||
573 | 70 | var prog_name = document.getElementById("program_name").value; | ||
574 | 71 | if (query_list.options[query_list.selectedIndex].value == "functions_calling"){ | ||
575 | 72 | document.getElementById("argument_1").innerHTML = "Function being called"; | ||
576 | 73 | document.getElementById("argument_2").innerHTML = ""; | ||
577 | 74 | document.getElementById("function_1").readOnly = false; | ||
578 | 75 | document.getElementById("function_2").readOnly = true; | ||
579 | 76 | document.getElementById("function_1").style.visibility = "visible"; | ||
580 | 77 | document.getElementById("function_2").style.visibility = "hidden"; | ||
581 | 78 | document.getElementById("function_2").value = null; | ||
582 | 79 | } | ||
583 | 80 | if (query_list.options[query_list.selectedIndex].value == "functions_called_by"){ | ||
584 | 81 | document.getElementById("argument_1").innerHTML = "Function calling"; | ||
585 | 82 | document.getElementById("argument_2").innerHTML = ""; | ||
586 | 83 | document.getElementById("function_1").readOnly = false; | ||
587 | 84 | document.getElementById("function_2").readOnly = true; | ||
588 | 85 | document.getElementById("function_1").style.visibility = "visible"; | ||
589 | 86 | document.getElementById("function_2").style.visibility = "hidden"; | ||
590 | 87 | document.getElementById("function_2").value = null; | ||
591 | 88 | } | ||
592 | 89 | if (query_list.options[query_list.selectedIndex].value == "call_paths"){ | ||
593 | 90 | document.getElementById("argument_1").innerHTML = "Function calling"; | ||
594 | 91 | document.getElementById("argument_2").innerHTML = "Function being called"; | ||
595 | 92 | document.getElementById("function_1").readOnly = false; | ||
596 | 93 | document.getElementById("function_2").readOnly = false; | ||
597 | 94 | document.getElementById("function_1").style.visibility = "visible"; | ||
598 | 95 | document.getElementById("function_2").style.visibility = "visible"; | ||
599 | 96 | } | ||
600 | 97 | if (query_list.options[query_list.selectedIndex].value == "shortest_path"){ | ||
601 | 98 | document.getElementById("argument_1").innerHTML = "Function calling"; | ||
602 | 99 | document.getElementById("argument_2").innerHTML = "Function being called"; | ||
603 | 100 | document.getElementById("function_1").readOnly = false; | ||
604 | 101 | document.getElementById("function_2").readOnly = false; | ||
605 | 102 | document.getElementById("function_1").style.visibility = "visible"; | ||
606 | 103 | document.getElementById("function_2").style.visibility = "visible"; | ||
607 | 104 | } | ||
608 | 105 | if (query_list.options[query_list.selectedIndex].value == "whole_program") { | ||
609 | 106 | document.getElementById("argument_1").innerHTML = "No arguments required."; | ||
610 | 107 | document.getElementById("argument_2").innerHTML = ""; | ||
611 | 108 | document.getElementById("function_1").readOnly = true; | ||
612 | 109 | document.getElementById("function_2").readOnly = true; | ||
613 | 110 | document.getElementById("function_1").style.visibility = "hidden"; | ||
614 | 111 | document.getElementById("function_2").style.visibility = "hidden"; | ||
615 | 112 | document.getElementById("function_1").value = null; | ||
616 | 113 | document.getElementById("function_2").value = null; | ||
617 | 114 | } | ||
618 | 115 | if (query_list.options[query_list.selectedIndex].value == "function_names"){ | ||
619 | 116 | document.getElementById("argument_1").innerHTML = "No arguments required."; | ||
620 | 117 | document.getElementById("argument_2").innerHTML = ""; | ||
621 | 118 | document.getElementById("function_1").readOnly = true; | ||
622 | 119 | document.getElementById("function_2").readOnly = true; | ||
623 | 120 | document.getElementById("function_1").style.visibility = "hidden"; | ||
624 | 121 | document.getElementById("function_2").style.visibility = "hidden"; | ||
625 | 122 | document.getElementById("function_1").value = null; | ||
626 | 123 | document.getElementById("function_2").value = null; | ||
627 | 124 | } | ||
628 | 125 | } | ||
629 | 126 | |||
630 | 127 | |||
631 | 128 | |||
632 | 129 | function execute_query(){ | ||
633 | 130 | //Returns error in alert window if query not executed properly, | ||
634 | 131 | //otherwise performs the query and outputs it | ||
635 | 132 | document.getElementById("output_image").src = ""; | ||
636 | 133 | document.getElementById("output_image").alt = "Please wait loading..."; | ||
637 | 134 | var query_id = document.getElementById("query_list").value; | ||
638 | 135 | if (query_id == "function_names"){ | ||
639 | 136 | //url for page containing all function names | ||
640 | 137 | var string = "/database_properties?program_name=" + | ||
641 | 138 | document.getElementById("program_name").value + "&query=functions"; | ||
642 | 139 | } | ||
643 | 140 | else{ | ||
644 | 141 | //If not function names we will want a graph as an output; | ||
645 | 142 | //url returns svg file of graph. | ||
646 | 143 | var string = "/output_graph.svg?program_name=" + | ||
647 | 144 | document.getElementById("program_name").value + | ||
648 | 145 | "&query=" + query_id + "&func1="; | ||
649 | 146 | string = string + document.getElementById("function_1").value + | ||
650 | 147 | "&func2=" + document.getElementById("function_2").value; | ||
651 | 148 | string = string + "&suppress_common=" + | ||
652 | 149 | document.getElementById('suppress_common').checked.toString(); | ||
653 | 150 | |||
654 | 151 | } | ||
655 | 152 | var xmlhttp = new XMLHttpRequest(); | ||
656 | 153 | xmlhttp.open("GET", string, true); | ||
657 | 154 | xmlhttp.send(); | ||
658 | 155 | xmlhttp.onreadystatechange = function(){ | ||
659 | 156 | if (xmlhttp.readyState == 4 && xmlhttp.status == 200){ | ||
660 | 157 | //readyState == 4 means query has finished executing. | ||
661 | 158 | //status == 200 means "GET"ing has been successful. | ||
662 | 159 | if (query_id == "function_names"){ | ||
663 | 160 | //Text output displayed in paragraph. | ||
664 | 161 | document.getElementById("function_names_output").innerHTML = | ||
665 | 162 | xmlhttp.responseText; | ||
666 | 163 | document.getElementById("function_names_output").style.visibility = | ||
667 | 164 | "visible" | ||
668 | 165 | //Clear current image if one exists. | ||
669 | 166 | document.getElementById("output_image").alt = ""; | ||
670 | 167 | document.getElementById("output_image").src = ""; | ||
671 | 168 | } | ||
672 | 169 | else{ | ||
673 | 170 | document.getElementById("function_names_output").style.visibility = | ||
674 | 171 | "hidden" | ||
675 | 172 | document.getElementById("output_image").src = string; | ||
676 | 173 | } | ||
677 | 174 | } | ||
678 | 175 | else if (xmlhttp.readyState == 4 && xmlhttp.status == 400){ | ||
679 | 176 | //Error occurred during query; display response. | ||
680 | 177 | document.getElementById("output_image").alt = ""; | ||
681 | 178 | window.alert(xmlhttp.responseText); | ||
682 | 179 | } | ||
683 | 180 | else if(xmlhttp.readyState == 4 && xmlhttp.status == 404){ | ||
684 | 181 | //Error occurred during query; display response. | ||
685 | 182 | document.getElementById("output_image").alt = ""; | ||
686 | 183 | window.alert(xmlhttp.responseText); | ||
687 | 184 | } | ||
688 | 185 | else if(xmlhttp.readyState ==4 && xmlhttp.status == 204){ | ||
689 | 186 | //Query executed correctly but graph returned is empty | ||
690 | 187 | document.getElementById("output_image").alt = ""; | ||
691 | 188 | window.alert("Graph returned was empty"); | ||
692 | 189 | } | ||
693 | 190 | else if (xmlhttp.readyState == 4 && xmlhttp.status == 502) { | ||
694 | 191 | //Error occurs if Neo4j isn't running | ||
695 | 192 | document.getElementById("output_image").alt = ""; | ||
696 | 193 | window.alert("Bad Gateway received - are you sure the database server is running?"); | ||
697 | 194 | } | ||
698 | 195 | else if(xmlhttp.readyState ==4){ | ||
699 | 196 | //query executed correctly | ||
700 | 197 | document.getElementById("output_image").alt = ""; | ||
701 | 198 | window.alert("Error not recognised"); | ||
702 | 199 | } | ||
703 | 200 | } | ||
704 | 201 | } | ||
705 | 0 | 202 | ||
706 | === added file 'resources/web/sextant.gif' | |||
707 | 1 | Binary files resources/web/sextant.gif 1970-01-01 00:00:00 +0000 and resources/web/sextant.gif 2014-08-15 12:04:08 +0000 differ | 203 | Binary files resources/web/sextant.gif 1970-01-01 00:00:00 +0000 and resources/web/sextant.gif 2014-08-15 12:04:08 +0000 differ |
708 | === added file 'resources/web/style_sheet.css' | |||
709 | --- resources/web/style_sheet.css 1970-01-01 00:00:00 +0000 | |||
710 | +++ resources/web/style_sheet.css 2014-08-15 12:04:08 +0000 | |||
711 | @@ -0,0 +1,36 @@ | |||
712 | 1 | pre { | ||
713 | 2 | color: green; | ||
714 | 3 | background: white; | ||
715 | 4 | font-family: monospace; | ||
716 | 5 | } | ||
717 | 6 | |||
718 | 7 | body { | ||
719 | 8 | font-family: Helvetica, sans-serif; | ||
720 | 9 | } | ||
721 | 10 | .button{ | ||
722 | 11 | display:block; width:100px; height:100px; border-radius:50px; font-size:15px; | ||
723 | 12 | color:#fff; line-height:100px; text-align:center; background:#FF0000 | ||
724 | 13 | } | ||
725 | 14 | |||
726 | 15 | .pos_img{ | ||
727 | 16 | position:absolute; | ||
728 | 17 | left:0px; | ||
729 | 18 | top:270px; | ||
730 | 19 | z-index:-1 | ||
731 | 20 | |||
732 | 21 | } | ||
733 | 22 | .pos_functions{ | ||
734 | 23 | position:absolute; | ||
735 | 24 | left:0px; | ||
736 | 25 | top:250px; | ||
737 | 26 | |||
738 | 27 | } | ||
739 | 28 | |||
740 | 29 | .button2{ | ||
741 | 30 | display:inline-block; width:200px; height:100px; border-radius:50px; | ||
742 | 31 | font-size:15px; color:#fff; line-height:100px; | ||
743 | 32 | text-align:center; background:#000000 | ||
744 | 33 | } | ||
745 | 34 | |||
746 | 35 | |||
747 | 36 | |||
748 | 0 | 37 | ||
749 | === added file 'setup.cfg' | |||
750 | --- setup.cfg 1970-01-01 00:00:00 +0000 | |||
751 | +++ setup.cfg 2014-08-15 12:04:08 +0000 | |||
752 | @@ -0,0 +1,2 @@ | |||
753 | 1 | [metadata] | ||
754 | 2 | description-file = README.md | ||
755 | 0 | \ No newline at end of file | 3 | \ No newline at end of file |
756 | 1 | 4 | ||
757 | === added file 'setup.py' | |||
758 | --- setup.py 1970-01-01 00:00:00 +0000 | |||
759 | +++ setup.py 2014-08-15 12:04:08 +0000 | |||
760 | @@ -0,0 +1,25 @@ | |||
761 | 1 | # ----------------------------------------- | ||
762 | 2 | # Sextant | ||
763 | 3 | # Copyright 2014, Ensoft Ltd. | ||
764 | 4 | # Author: James Harkin, using work from Patrick Stevens and James Harkin | ||
765 | 5 | # ----------------------------------------- | ||
766 | 6 | # | ||
767 | 7 | |||
768 | 8 | import glob | ||
769 | 9 | import os | ||
770 | 10 | from setuptools import setup | ||
771 | 11 | |||
772 | 12 | setup( | ||
773 | 13 | name='Sextant', | ||
774 | 14 | version='1.0', | ||
775 | 15 | description= 'Navigating the C', | ||
776 | 16 | url='http://open.ensoft.co.uk/Sextant', | ||
777 | 17 | license='Simplified BSD License', | ||
778 | 18 | packages=['sextant', 'sextant.web', 'resources', 'etc'], | ||
779 | 19 | package_dir={'sextant': 'src/sextant', 'resources': 'resources', 'etc': 'etc'}, | ||
780 | 20 | scripts=['bin/sextant'], | ||
781 | 21 | install_requires=['neo4jrestclient', 'twisted'], | ||
782 | 22 | package_data={'resources': ['web/*'], 'etc': ['*.conf']}, | ||
783 | 23 | ) | ||
784 | 24 | |||
785 | 25 | |||
786 | 0 | 26 | ||
787 | === added directory 'src' | |||
788 | === added directory 'src/sextant' | |||
789 | === added file 'src/sextant/__init__.py' | |||
790 | --- src/sextant/__init__.py 1970-01-01 00:00:00 +0000 | |||
791 | +++ src/sextant/__init__.py 2014-08-15 12:04:08 +0000 | |||
792 | @@ -0,0 +1,21 @@ | |||
793 | 1 | # ----------------------------------------- | ||
794 | 2 | # Sextant | ||
795 | 3 | # Copyright 2014, Ensoft Ltd. | ||
796 | 4 | # Author: Patrick Stevens, James Harkin | ||
797 | 5 | # ----------------------------------------- | ||
798 | 6 | """Program call graph recording and query framework.""" | ||
799 | 7 | |||
800 | 8 | from . import errors | ||
801 | 9 | from . import pyinput | ||
802 | 10 | |||
803 | 11 | __all__ = ( | ||
804 | 12 | "SextantConnection", | ||
805 | 13 | ) + ( | ||
806 | 14 | errors.__all__ + | ||
807 | 15 | pyinput.__all__ | ||
808 | 16 | ) | ||
809 | 17 | |||
810 | 18 | from .db_api import SextantConnection | ||
811 | 19 | from .errors import * | ||
812 | 20 | from .pyinput import * | ||
813 | 21 | |||
814 | 0 | 22 | ||
815 | === added file 'src/sextant/__main__.py' | |||
816 | --- src/sextant/__main__.py 1970-01-01 00:00:00 +0000 | |||
817 | +++ src/sextant/__main__.py 2014-08-15 12:04:08 +0000 | |||
818 | @@ -0,0 +1,220 @@ | |||
819 | 1 | # ----------------------------------------- | ||
820 | 2 | # Sextant | ||
821 | 3 | # Copyright 2014, Ensoft Ltd. | ||
822 | 4 | # Author: James Harkin, using work from Patrick Stevens and James Harkin | ||
823 | 5 | # ----------------------------------------- | ||
824 | 6 | #invokes Sextant and argparse | ||
825 | 7 | |||
826 | 8 | from __future__ import absolute_import, print_function | ||
827 | 9 | |||
828 | 10 | import argparse | ||
829 | 11 | try: | ||
830 | 12 | import ConfigParser | ||
831 | 13 | except ImportError: | ||
832 | 14 | import configparser as ConfigParser | ||
833 | 15 | import logging | ||
834 | 16 | import logging.config | ||
835 | 17 | import os | ||
836 | 18 | import sys | ||
837 | 19 | |||
838 | 20 | from . import update_db | ||
839 | 21 | from . import query | ||
840 | 22 | from . import db_api | ||
841 | 23 | |||
842 | 24 | |||
843 | 25 | # @@@ Logging level should be configurable (with some structure to setting up | ||
844 | 26 | # logging). | ||
845 | 27 | logging.config.dictConfig({ | ||
846 | 28 | "version": 1, | ||
847 | 29 | "handlers": { | ||
848 | 30 | "console": { | ||
849 | 31 | "class": "logging.StreamHandler", | ||
850 | 32 | "level": logging.INFO, | ||
851 | 33 | "stream": "ext://sys.stderr", | ||
852 | 34 | }, | ||
853 | 35 | }, | ||
854 | 36 | "root": { | ||
855 | 37 | "level": logging.DEBUG, | ||
856 | 38 | "handlers": ["console"], | ||
857 | 39 | }, | ||
858 | 40 | }) | ||
859 | 41 | log = logging.getLogger() | ||
860 | 42 | |||
861 | 43 | |||
862 | 44 | def get_config_file(): | ||
863 | 45 | # get the config file option for neo4j server location and web port number | ||
864 | 46 | _ROOT = os.path.abspath(os.path.dirname(__file__)) | ||
865 | 47 | def get_data(path, file_name): | ||
866 | 48 | return os.path.join(_ROOT, path, file_name) | ||
867 | 49 | |||
868 | 50 | home_config = os.path.expanduser(os.path.join("~", ".sextant.conf")) | ||
869 | 51 | env_config = os.environ.get("SEXTANT_CONFIG", "") | ||
870 | 52 | example_config = get_data('../etc', 'sextant.conf') | ||
871 | 53 | |||
872 | 54 | try: | ||
873 | 55 | conffile = next(p | ||
874 | 56 | for p in (home_config, env_config, example_config) | ||
875 | 57 | if os.path.exists(p)) | ||
876 | 58 | except StopIteration: | ||
877 | 59 | #No config files accessable | ||
878 | 60 | if "SEXTANT_CONFIG" in os.environ: | ||
879 | 61 | #SEXTANT_CONFIG environment variable is set | ||
880 | 62 | log.error("SEXTANT_CONFIG file %r doesn't exist.", env_config) | ||
881 | 63 | log.error("Sextant requires a configuration file.") | ||
882 | 64 | sys.exit(1) | ||
883 | 65 | |||
884 | 66 | log.info("Sextant is using config file %s", conffile) | ||
885 | 67 | return conffile | ||
886 | 68 | |||
887 | 69 | conffile = get_config_file() | ||
888 | 70 | |||
889 | 71 | conf = ConfigParser.ConfigParser() | ||
890 | 72 | conf.read(conffile) | ||
891 | 73 | |||
892 | 74 | #remote_neo4j = 'http://localhost:7474' | ||
893 | 75 | #web_port = 2905 | ||
894 | 76 | #common_def = 10 # definition of a 'common' node | ||
895 | 77 | try: | ||
896 | 78 | options = conf.options('Neo4j') | ||
897 | 79 | except ConfigParser.NoSectionError: | ||
898 | 80 | pass | ||
899 | 81 | else: | ||
900 | 82 | try: | ||
901 | 83 | remote_neo4j = conf.get('Neo4j', 'remote_neo4j') | ||
902 | 84 | except ConfigParser.NoOptionError: | ||
903 | 85 | pass | ||
904 | 86 | |||
905 | 87 | try: | ||
906 | 88 | web_port = conf.get('Neo4j', 'port') | ||
907 | 89 | except ConfigParser.NoOptionError: | ||
908 | 90 | pass | ||
909 | 91 | |||
910 | 92 | try: | ||
911 | 93 | common_def = conf.get('Neo4j', 'common_function_calls') | ||
912 | 94 | except ConfigParser.NoOptionError: | ||
913 | 95 | common_def = 10 | ||
914 | 96 | |||
915 | 97 | argumentparser = argparse.ArgumentParser(description="Invoke part of the SEXTANT program") | ||
916 | 98 | subparsers = argumentparser.add_subparsers(title="subcommands") | ||
917 | 99 | |||
918 | 100 | #set what will be defined as a "common function" | ||
919 | 101 | db_api.set_common_cutoff(common_def) | ||
920 | 102 | |||
921 | 103 | parsers = dict() | ||
922 | 104 | |||
923 | 105 | # add each subparser in turn to the parsers dictionary | ||
924 | 106 | |||
925 | 107 | parsers['add'] = subparsers.add_parser('add_program', help="add a program to the database") | ||
926 | 108 | parsers['add'].add_argument('--input-file', required=True, metavar="FILE_NAME", | ||
927 | 109 | help="name of file to be put into database", | ||
928 | 110 | type=str, nargs=1) | ||
929 | 111 | parsers['add'].add_argument('--set-file-name', metavar="FILE_NAME", | ||
930 | 112 | help="string to store this program under", type=str, | ||
931 | 113 | nargs=1) | ||
932 | 114 | parsers['add'].add_argument('--not-object-file', | ||
933 | 115 | help='default False, if the input file is an object to be disassembled', | ||
934 | 116 | action='store_true') | ||
935 | 117 | |||
936 | 118 | parsers['delete'] = subparsers.add_parser('delete_program', | ||
937 | 119 | help="delete a program from the database") | ||
938 | 120 | parsers['delete'].add_argument('--program-name', required=True, metavar="PROG_NAME", | ||
939 | 121 | help="name of program as stored in the database", | ||
940 | 122 | type=str, nargs=1) | ||
941 | 123 | |||
942 | 124 | parsers['query'] = subparsers.add_parser('query', | ||
943 | 125 | help="make a query of the database") | ||
944 | 126 | parsers['query'].add_argument('--program-name', metavar="PROG_NAME", | ||
945 | 127 | help="name of program as stored in the database", | ||
946 | 128 | type=str, nargs=1) | ||
947 | 129 | parsers['query'].add_argument('--query', required=True, metavar="QUERY", | ||
948 | 130 | help="functions-calling, functions-called-by, " | ||
949 | 131 | "calls-between, whole-graph, shortest-path, " | ||
950 | 132 | "return-all-program-names or " | ||
951 | 133 | "return-all-function-names; if the latter, " | ||
952 | 134 | "supply argument --program-name", | ||
953 | 135 | type=str, nargs=1) | ||
954 | 136 | parsers['query'].add_argument('--funcs', metavar='FUNCS', | ||
955 | 137 | help='functions to pass to the query', | ||
956 | 138 | type=str, nargs='+') | ||
957 | 139 | parsers['query'].add_argument('--suppress-common', metavar='BOOL', | ||
958 | 140 | help='suppress commonly called functions (True or False)', | ||
959 | 141 | type=str, nargs=1) | ||
960 | 142 | |||
961 | 143 | parsers['web'] = subparsers.add_parser('web', help="start the web server") | ||
962 | 144 | parsers['web'].add_argument('--port', metavar='PORT', type=int, | ||
963 | 145 | help='port on which to serve Sextant Web', | ||
964 | 146 | default=web_port) | ||
965 | 147 | |||
966 | 148 | for parser_key in parsers: | ||
967 | 149 | parsers[parser_key].add_argument('--remote-neo4j', metavar="URL", nargs=1, | ||
968 | 150 | help="URL of neo4j server", type=str, | ||
969 | 151 | default=remote_neo4j) | ||
970 | 152 | |||
971 | 153 | def _start_web(args): | ||
972 | 154 | # Don't import at top level -- this makes twisted dependency semi-optional, | ||
973 | 155 | # allowing non-web functionality to work with Python 3. | ||
974 | 156 | from .web import server | ||
975 | 157 | log.info("Serving site on port %s", args.port) | ||
976 | 158 | server.serve_site(input_database_url=args.remote_neo4j, port=args.port) | ||
977 | 159 | |||
978 | 160 | parsers['web'].set_defaults(func=_start_web) | ||
979 | 161 | |||
980 | 162 | def add_file(namespace): | ||
981 | 163 | |||
982 | 164 | try: | ||
983 | 165 | alternative_name = namespace.set_file_name[0] | ||
984 | 166 | except TypeError: | ||
985 | 167 | alternative_name = None | ||
986 | 168 | |||
987 | 169 | not_object_file = namespace.not_object_file | ||
988 | 170 | # the default is "yes, this is an object file" if not-object-file was | ||
989 | 171 | # unsupplied | ||
990 | 172 | |||
991 | 173 | update_db.upload_program(namespace.input_file[0], | ||
992 | 174 | namespace.remote_neo4j, | ||
993 | 175 | alternative_name, not_object_file) | ||
994 | 176 | |||
995 | 177 | |||
996 | 178 | def delete_file(namespace): | ||
997 | 179 | update_db.delete_program(namespace.program_name[0], | ||
998 | 180 | namespace.remote_neo4j) | ||
999 | 181 | |||
1000 | 182 | parsers['add'].set_defaults(func=add_file) | ||
1001 | 183 | parsers['delete'].set_defaults(func=delete_file) | ||
1002 | 184 | |||
1003 | 185 | |||
1004 | 186 | def make_query(namespace): | ||
1005 | 187 | |||
1006 | 188 | arg1 = None | ||
1007 | 189 | arg2 = None | ||
1008 | 190 | try: | ||
1009 | 191 | arg1 = namespace.funcs[0] | ||
1010 | 192 | arg2 = namespace.funcs[1] | ||
1011 | 193 | except TypeError: | ||
1012 | 194 | pass | ||
1013 | 195 | |||
1014 | 196 | try: | ||
1015 | 197 | program_name = namespace.program_name[0] | ||
1016 | 198 | except TypeError: | ||
1017 | 199 | program_name = None | ||
1018 | 200 | |||
1019 | 201 | try: | ||
1020 | 202 | suppress_common = namespace.suppress_common[0] | ||
1021 | 203 | except TypeError: | ||
1022 | 204 | suppress_common = False | ||
1023 | 205 | |||
1024 | 206 | query.query(remote_neo4j=namespace.remote_neo4j, | ||
1025 | 207 | input_query=namespace.query[0], | ||
1026 | 208 | program_name=program_name, argument_1=arg1, argument_2=arg2, | ||
1027 | 209 | suppress_common=suppress_common) | ||
1028 | 210 | |||
1029 | 211 | parsers['query'].set_defaults(func=make_query) | ||
1030 | 212 | |||
1031 | 213 | # parse the arguments | ||
1032 | 214 | |||
1033 | 215 | parsed = argumentparser.parse_args() | ||
1034 | 216 | |||
1035 | 217 | # call the appropriate function | ||
1036 | 218 | |||
1037 | 219 | parsed.func(parsed) | ||
1038 | 220 | |||
1039 | 0 | 221 | ||
1040 | === added file 'src/sextant/db_api.py' | |||
1041 | --- src/sextant/db_api.py 1970-01-01 00:00:00 +0000 | |||
1042 | +++ src/sextant/db_api.py 2014-08-15 12:04:08 +0000 | |||
1043 | @@ -0,0 +1,610 @@ | |||
1044 | 1 | # ----------------------------------------- | ||
1045 | 2 | # Sextant | ||
1046 | 3 | # Copyright 2014, Ensoft Ltd. | ||
1047 | 4 | # Author: Patrick Stevens, using work from Patrick Stevens and James Harkin | ||
1048 | 5 | # ----------------------------------------- | ||
1049 | 6 | # API to interact with a Neo4J server: upload, query and delete programs in a DB | ||
1050 | 7 | |||
1051 | 8 | __all__ = ("Validator", "AddToDatabase", "FunctionQueryResult", "Function", | ||
1052 | 9 | "SextantConnection") | ||
1053 | 10 | |||
1054 | 11 | import re # for validation of function/program names | ||
1055 | 12 | import logging | ||
1056 | 13 | |||
1057 | 14 | from neo4jrestclient.client import GraphDatabase | ||
1058 | 15 | import neo4jrestclient.client as client | ||
1059 | 16 | |||
1060 | 17 | COMMON_CUTOFF = 10 | ||
1061 | 18 | # a function is deemed 'common' if it has more than this | ||
1062 | 19 | # many connections | ||
1063 | 20 | |||
1064 | 21 | |||
1065 | 22 | class Validator(): | ||
1066 | 23 | """ Sanitises/checks strings, to prevent Cypher injection attacks""" | ||
1067 | 24 | |||
1068 | 25 | @staticmethod | ||
1069 | 26 | def validate(input_): | ||
1070 | 27 | """ | ||
1071 | 28 | Checks whether we can allow a string to be passed into a Cypher query. | ||
1072 | 29 | :param input_: the string we wish to validate | ||
1073 | 30 | :return: bool(the string is allowed) | ||
1074 | 31 | """ | ||
1075 | 32 | regex = re.compile(r'^[A-Za-z0-9\-:\.\$_@\*\(\)%\+,]+$') | ||
1076 | 33 | return bool(regex.match(input_)) | ||
1077 | 34 | |||
1078 | 35 | @staticmethod | ||
1079 | 36 | def sanitise(input_): | ||
1080 | 37 | """ | ||
1081 | 38 | Strips harmful characters from the given string. | ||
1082 | 39 | :param input_: string to sanitise | ||
1083 | 40 | :return: the sanitised string | ||
1084 | 41 | """ | ||
1085 | 42 | return re.sub(r'[^\.\-_a-zA-Z0-9]+', '', input_) | ||
1086 | 43 | |||
1087 | 44 | |||
1088 | 45 | class AddToDatabase(): | ||
1089 | 46 | """Updates the database, adding functions/calls to a given program""" | ||
1090 | 47 | |||
1091 | 48 | def __init__(self, program_name='', sextant_connection=None): | ||
1092 | 49 | """ | ||
1093 | 50 | Object which can be used to add functions and calls to a new program | ||
1094 | 51 | :param program_name: the name of the new program to be created | ||
1095 | 52 | (must already be validated against Validator) | ||
1096 | 53 | :param sextant_connection: the SextantConnection to use for connections | ||
1097 | 54 | """ | ||
1098 | 55 | # program_name must be alphanumeric, to avoid injection attacks easily | ||
1099 | 56 | if not Validator.validate(program_name): | ||
1100 | 57 | return | ||
1101 | 58 | |||
1102 | 59 | self.program_name = program_name | ||
1103 | 60 | self.parent_database_connection = sextant_connection | ||
1104 | 61 | self._functions = {} | ||
1105 | 62 | self._new_tx = None | ||
1106 | 63 | |||
1107 | 64 | if self.parent_database_connection: | ||
1108 | 65 | # we'll locally use db for short | ||
1109 | 66 | db = self.parent_database_connection._db | ||
1110 | 67 | |||
1111 | 68 | parent_function = db.nodes.create(name=program_name, type='program') | ||
1112 | 69 | self._parent_id = parent_function.id | ||
1113 | 70 | |||
1114 | 71 | self._new_tx = db.transaction(using_globals=False, for_query=True) | ||
1115 | 72 | |||
1116 | 73 | self._connections = [] | ||
1117 | 74 | |||
1118 | 75 | def add_function(self, function_name): | ||
1119 | 76 | """ | ||
1120 | 77 | Adds a function to the program, ready to be sent to the remote database. | ||
1121 | 78 | If the function name is already in use, this method effectively does | ||
1122 | 79 | nothing and returns True. | ||
1123 | 80 | |||
1124 | 81 | :param function_name: a string which must be alphanumeric | ||
1125 | 82 | :return: True if the request succeeded, False otherwise | ||
1126 | 83 | """ | ||
1127 | 84 | if not Validator.validate(function_name): | ||
1128 | 85 | return False | ||
1129 | 86 | if self.class_contains_function(function_name): | ||
1130 | 87 | return True | ||
1131 | 88 | |||
1132 | 89 | if function_name[-4:] == "@plt": | ||
1133 | 90 | display_name = function_name[:-4] | ||
1134 | 91 | function_group = "plt_stub" | ||
1135 | 92 | elif function_name[:20] == "_._function_pointer_": | ||
1136 | 93 | display_name = function_name | ||
1137 | 94 | function_group = "function_pointer" | ||
1138 | 95 | else: | ||
1139 | 96 | display_name = function_name | ||
1140 | 97 | function_group = "normal" | ||
1141 | 98 | |||
1142 | 99 | query = ('START n = node({}) ' | ||
1143 | 100 | 'CREATE (n)-[:subject]->(m:func {{type: "{}", name: "{}"}})') | ||
1144 | 101 | query = query.format(self._parent_id, function_group, display_name) | ||
1145 | 102 | |||
1146 | 103 | self._new_tx.append(query) | ||
1147 | 104 | |||
1148 | 105 | self._functions[function_name] = function_name | ||
1149 | 106 | |||
1150 | 107 | return True | ||
1151 | 108 | |||
1152 | 109 | def class_contains_function(self, function_to_find): | ||
1153 | 110 | """ | ||
1154 | 111 | Checks whether we contain a function with a given name. | ||
1155 | 112 | :param function_to_find: string name of the function we wish to look up | ||
1156 | 113 | :return: bool(the function exists in this AddToDatabase) | ||
1157 | 114 | """ | ||
1158 | 115 | return function_to_find in self._functions | ||
1159 | 116 | |||
1160 | 117 | def class_contains_call(self, function_calling, function_called): | ||
1161 | 118 | """ | ||
1162 | 119 | Checks whether we contain a call between the two named functions. | ||
1163 | 120 | :param function_calling: string name of the calling-function | ||
1164 | 121 | :param function_called: string name of the called function | ||
1165 | 122 | :return: bool(function_calling calls function_called in us) | ||
1166 | 123 | """ | ||
1167 | 124 | return (function_calling, function_called) in self._connections | ||
1168 | 125 | |||
1169 | 126 | def add_function_call(self, fn_calling, fn_called): | ||
1170 | 127 | """ | ||
1171 | 128 | Adds a function call to the program, ready to be sent to the database. | ||
1172 | 129 | Effectively does nothing if there is already a function call between | ||
1173 | 130 | these two functions. | ||
1174 | 131 | Function names must be alphanumeric for easy security purposes; | ||
1175 | 132 | returns False if they fail validation. | ||
1176 | 133 | :param fn_calling: the name of the calling-function as a string. | ||
1177 | 134 | It should already exist in the AddToDatabase; if it does not, | ||
1178 | 135 | this method will create a stub for it. | ||
1179 | 136 | :param fn_called: name of the function called by fn_calling. | ||
1180 | 137 | If it does not exist, we create a stub representation for it. | ||
1181 | 138 | :return: True if successful, False otherwise | ||
1182 | 139 | """ | ||
1183 | 140 | if not all((Validator.validate(fn_calling), | ||
1184 | 141 | Validator.validate(fn_called))): | ||
1185 | 142 | return False | ||
1186 | 143 | |||
1187 | 144 | if not self.class_contains_function(fn_called): | ||
1188 | 145 | self.add_function(fn_called) | ||
1189 | 146 | if not self.class_contains_function(fn_calling): | ||
1190 | 147 | self.add_function(fn_calling) | ||
1191 | 148 | |||
1192 | 149 | if not self.class_contains_call(fn_calling, fn_called): | ||
1193 | 150 | query = ('START p = node({}) ' | ||
1194 | 151 | 'MATCH (p)-[:subject]->(n) WHERE n.name = "{}" ' | ||
1195 | 152 | 'MATCH (p)-[:subject]->(m) WHERE m.name = "{}" ' | ||
1196 | 153 | 'CREATE (n)-[:calls]->(m)') | ||
1197 | 154 | query = query.format(self._parent_id, fn_calling, fn_called) | ||
1198 | 155 | self._new_tx.append(query) | ||
1199 | 156 | |||
1200 | 157 | self._connections.append((fn_calling, fn_called)) | ||
1201 | 158 | |||
1202 | 159 | return True | ||
1203 | 160 | |||
1204 | 161 | def commit(self): | ||
1205 | 162 | """ | ||
1206 | 163 | Call this when you are finished with the object. | ||
1207 | 164 | Changes are not synced to the remote database until this is called. | ||
1208 | 165 | """ | ||
1209 | 166 | self._new_tx.commit() | ||
1210 | 167 | |||
1211 | 168 | |||
1212 | 169 | class FunctionQueryResult: | ||
1213 | 170 | """A graph of function calls arising as the result of a Neo4J query.""" | ||
1214 | 171 | |||
1215 | 172 | def __init__(self, parent_db, program_name='', rest_output=None): | ||
1216 | 173 | self.program_name = program_name | ||
1217 | 174 | self._parent_db_connection = parent_db | ||
1218 | 175 | self.functions = self._rest_node_output_to_graph(rest_output) | ||
1219 | 176 | self._update_common_functions() | ||
1220 | 177 | |||
1221 | 178 | def __eq__(self, other): | ||
1222 | 179 | # we make a dictionary so that we can perform easy comparison | ||
1223 | 180 | selfdict = {func.name: func for func in self.functions} | ||
1224 | 181 | otherdict = {func.name: func for func in other.functions} | ||
1225 | 182 | |||
1226 | 183 | return self.program_name == other.program_name and selfdict == otherdict | ||
1227 | 184 | |||
1228 | 185 | def _update_common_functions(self): | ||
1229 | 186 | """ | ||
1230 | 187 | Loop over all functions: increment the called-by count of their callees. | ||
1231 | 188 | """ | ||
1232 | 189 | for func in self.functions: | ||
1233 | 190 | for called in func.functions_i_call: | ||
1234 | 191 | called.number_calling_me += 1 | ||
1235 | 192 | |||
1236 | 193 | def _rest_node_output_to_graph(self, rest_output): | ||
1237 | 194 | """ | ||
1238 | 195 | Convert the output of a REST API query into our internal representation. | ||
1239 | 196 | :param rest_output: output of the REST call as a Neo4j QuerySequence | ||
1240 | 197 | :return: iterable of <Function>s ready to initialise self.functions. | ||
1241 | 198 | """ | ||
1242 | 199 | |||
1243 | 200 | if rest_output is None or not rest_output.elements: | ||
1244 | 201 | return [] | ||
1245 | 202 | |||
1246 | 203 | # how we store this is: a dict | ||
1247 | 204 | # with keys 'functionname' | ||
1248 | 205 | # and values [the function object we will use, | ||
1249 | 206 | # and a set of (function names this function calls), | ||
1250 | 207 | # and numeric ID of this node in the Neo4J database] | ||
1251 | 208 | |||
1252 | 209 | result = {} | ||
1253 | 210 | |||
1254 | 211 | # initial pass for names of functions | ||
1255 | 212 | |||
1256 | 213 | # if the following assertion failed, we've probably called db.query | ||
1257 | 214 | # to get it to not return client.Node objects, which is wrong. | ||
1258 | 215 | # we attempt to handle this a bit later; this should never arise, but | ||
1259 | 216 | # we can cope with it happening in some cases, like the test suite | ||
1260 | 217 | |||
1261 | 218 | if type(rest_output.elements) is not list: | ||
1262 | 219 | logging.warning('Not a list: {}'.format(type(rest_output.elements))) | ||
1263 | 220 | |||
1264 | 221 | for node_list in rest_output.elements: | ||
1265 | 222 | assert(isinstance(node_list, list)) | ||
1266 | 223 | for node in node_list: | ||
1267 | 224 | if isinstance(node, client.Node): | ||
1268 | 225 | name = node.properties['name'] | ||
1269 | 226 | node_id = node.id | ||
1270 | 227 | node_type = node.properties['type'] | ||
1271 | 228 | else: # this is the handling we mentioned earlier; | ||
1272 | 229 | # we are a dictionary instead of a list, as for some | ||
1273 | 230 | # reason we've returned Raw rather than Node data. | ||
1274 | 231 | # We should never reach this code, but just in case. | ||
1275 | 232 | name = node['data']['name'] | ||
1276 | 233 | # hacky workaround to get the id | ||
1277 | 234 | node_id = node['self'].split('/')[-1] | ||
1278 | 235 | node_type = node['data']['type'] | ||
1279 | 236 | |||
1280 | 237 | result[name] = [Function(self.program_name, | ||
1281 | 238 | function_name=name, | ||
1282 | 239 | function_type=node_type), | ||
1283 | 240 | set(), | ||
1284 | 241 | node_id] | ||
1285 | 242 | |||
1286 | 243 | # end initialisation of names-dictionary | ||
1287 | 244 | |||
1288 | 245 | if self._parent_db_connection is not None: | ||
1289 | 246 | # This is the normal case, of extracting results from a server. | ||
1290 | 247 | # We leave the other case in because it is useful for unit testing. | ||
1291 | 248 | |||
1292 | 249 | # We collect the name-name pairs of caller-callee, batched for speed | ||
1293 | 250 | new_tx = self._parent_db_connection.transaction(using_globals=False, | ||
1294 | 251 | for_query=True) | ||
1295 | 252 | for index in result: | ||
1296 | 253 | q = ("START n=node({})" | ||
1297 | 254 | "MATCH n-[calls:calls]->(m)" | ||
1298 | 255 | "RETURN n.name, m.name").format(result[index][2]) | ||
1299 | 256 | new_tx.append(q) | ||
1300 | 257 | |||
1301 | 258 | logging.debug('exec') | ||
1302 | 259 | results = new_tx.execute() | ||
1303 | 260 | |||
1304 | 261 | # results is a list of query results, each of those being a list of | ||
1305 | 262 | # calls. | ||
1306 | 263 | |||
1307 | 264 | for call_list in results: | ||
1308 | 265 | if call_list: | ||
1309 | 266 | # call_list has element 0 being an arbitrary call this | ||
1310 | 267 | # function makes; element 0 of that call is the name of the | ||
1311 | 268 | # function itself. Think {{'orig', 'b'}, {'orig', 'c'}}. | ||
1312 | 269 | orig = call_list[0][0] | ||
1313 | 270 | # result['orig'] is [<Function>, ('callee1','callee2')] | ||
1314 | 271 | result[orig][1] |= set(list(zip(*call_list.elements))[1]) | ||
1315 | 272 | # recall: set union is denoted by | | ||
1316 | 273 | |||
1317 | 274 | else: | ||
1318 | 275 | # we don't have a parent database connection. | ||
1319 | 276 | # This has probably arisen because we created this object from a | ||
1320 | 277 | # test suite, or something like that. | ||
1321 | 278 | for node in rest_output.elements: | ||
1322 | 279 | node_name = node[0].properties['name'] | ||
1323 | 280 | result[node_name][1] |= {relationship.end.properties['name'] | ||
1324 | 281 | for relationship in node[0].relationships.outgoing()} | ||
1325 | 282 | |||
1326 | 283 | logging.debug('Relationships complete.') | ||
1327 | 284 | |||
1328 | 285 | # named_function takes a function name and returns the Function object | ||
1329 | 286 | # with that name, or None if none exists. | ||
1330 | 287 | named_function = lambda name: result[name][0] if name in result else None | ||
1331 | 288 | |||
1332 | 289 | for function, calls, node_id in result.values(): | ||
1333 | 290 | what_i_call = [named_function(name) | ||
1334 | 291 | for name in calls | ||
1335 | 292 | if named_function(name) is not None] | ||
1336 | 293 | function.functions_i_call = what_i_call | ||
1337 | 294 | |||
1338 | 295 | return [list_element[0] | ||
1339 | 296 | for list_element in result.values() | ||
1340 | 297 | if list_element[0]] | ||
1341 | 298 | |||
1342 | 299 | def get_functions(self): | ||
1343 | 300 | """ | ||
1344 | 301 | :return: a list of Function objects present in the query result | ||
1345 | 302 | """ | ||
1346 | 303 | return self.functions | ||
1347 | 304 | |||
1348 | 305 | def get_function(self, name): | ||
1349 | 306 | """ | ||
1350 | 307 | Given a function name, returns the Function object which has that name. | ||
1351 | 308 | If no function with that name exists, returns None. | ||
1352 | 309 | """ | ||
1353 | 310 | func_list = [func for func in self.functions if func.name == name] | ||
1354 | 311 | return None if len(func_list) == 0 else func_list[0] | ||
1355 | 312 | |||
1356 | 313 | |||
1357 | 314 | def set_common_cutoff(common_def): | ||
1358 | 315 | """ | ||
1359 | 316 | Sets the number of incoming connections at which we deem a function 'common' | ||
1360 | 317 | Default is 10 (which is used if this method is never called). | ||
1361 | 318 | :param common_def: number of incoming connections | ||
1362 | 319 | """ | ||
1363 | 320 | global COMMON_CUTOFF | ||
1364 | 321 | COMMON_CUTOFF = common_def | ||
1365 | 322 | |||
1366 | 323 | |||
1367 | 324 | class Function(object): | ||
1368 | 325 | """Represents a function which might appear in a FunctionQueryResult.""" | ||
1369 | 326 | |||
1370 | 327 | def __eq__(self, other): | ||
1371 | 328 | funcs_i_call_list = {func.name for func in self.functions_i_call} | ||
1372 | 329 | funcs_other_calls_list = {func.name for func in other.functions_i_call} | ||
1373 | 330 | |||
1374 | 331 | return (self.parent_program == other.parent_program | ||
1375 | 332 | and self.name == other.name | ||
1376 | 333 | and funcs_i_call_list == funcs_other_calls_list | ||
1377 | 334 | and self.attributes == other.attributes) | ||
1378 | 335 | |||
1379 | 336 | @property | ||
1380 | 337 | def number_calling_me(self): | ||
1381 | 338 | return self._number_calling_me | ||
1382 | 339 | |||
1383 | 340 | @number_calling_me.setter | ||
1384 | 341 | def number_calling_me(self, value): | ||
1385 | 342 | self._number_calling_me = value | ||
1386 | 343 | self.is_common = (self._number_calling_me > COMMON_CUTOFF) | ||
1387 | 344 | |||
1388 | 345 | def __init__(self, program_name='', function_name='', function_type=''): | ||
1389 | 346 | self.parent_program = program_name | ||
1390 | 347 | self.attributes = [] | ||
1391 | 348 | self.type = function_type | ||
1392 | 349 | self.functions_i_call = [] | ||
1393 | 350 | self.name = function_name | ||
1394 | 351 | self.is_common = False | ||
1395 | 352 | self._number_calling_me = 0 | ||
1396 | 353 | # care: _number_calling_me is not automatically updated, except by | ||
1397 | 354 | # any invocation of FunctionQueryResult._update_common_functions. | ||
1398 | 355 | |||
1399 | 356 | |||
1400 | 357 | class SextantConnection: | ||
1401 | 358 | """ | ||
1402 | 359 | RESTful connection to a remote database. | ||
1403 | 360 | It can be used to create/delete/query programs. | ||
1404 | 361 | """ | ||
1405 | 362 | |||
1406 | 363 | def __init__(self, url): | ||
1407 | 364 | self.url = url | ||
1408 | 365 | self._db = GraphDatabase(url) | ||
1409 | 366 | |||
1410 | 367 | def new_program(self, name_of_program): | ||
1411 | 368 | """ | ||
1412 | 369 | Request that the remote database create a new program with the given name. | ||
1413 | 370 | This procedure will create a new program remotely; you can manipulate | ||
1414 | 371 | that program using the returned AddToDatabase object. | ||
1415 | 372 | The name can appear in the database already, but this is not recommended | ||
1416 | 373 | because then delete_program will not know which to delete. Check first | ||
1417 | 374 | using self.check_program_exists. | ||
1418 | 375 | The name specified must pass Validator.validate()ion; this is a measure | ||
1419 | 376 | to prevent Cypher injection attacks. | ||
1420 | 377 | :param name_of_program: string program name | ||
1421 | 378 | :return: AddToDatabase instance if successful | ||
1422 | 379 | """ | ||
1423 | 380 | |||
1424 | 381 | if not Validator.validate(name_of_program): | ||
1425 | 382 | raise ValueError( | ||
1426 | 383 | "{} is not a valid program name".format(name_of_program)) | ||
1427 | 384 | |||
1428 | 385 | return AddToDatabase(sextant_connection=self, | ||
1429 | 386 | program_name=name_of_program) | ||
1430 | 387 | |||
1431 | 388 | def delete_program(self, name_of_program): | ||
1432 | 389 | """ | ||
1433 | 390 | Request that the remote database delete a specified program. | ||
1434 | 391 | :param name_of_program: a string which must be alphanumeric only | ||
1435 | 392 | :return: bool(request succeeded) | ||
1436 | 393 | """ | ||
1437 | 394 | if not Validator.validate(name_of_program): | ||
1438 | 395 | return False | ||
1439 | 396 | |||
1440 | 397 | q = """MATCH (n) WHERE n.name= "{}" AND n.type="program" | ||
1441 | 398 | OPTIONAL MATCH (n)-[r]-(b) OPTIONAL MATCH (b)-[rel]-() | ||
1442 | 399 | DELETE b,rel DELETE n, r""".format(name_of_program) | ||
1443 | 400 | |||
1444 | 401 | self._db.query(q) | ||
1445 | 402 | |||
1446 | 403 | return True | ||
1447 | 404 | |||
1448 | 405 | def _execute_query(self, prog_name='', query=''): | ||
1449 | 406 | """ | ||
1450 | 407 | Executes a Cypher query against the remote database. | ||
1451 | 408 | Note that this returns a FunctionQueryResult, so is unsuitable for any | ||
1452 | 409 | other expected outputs (such as lists of names). For those instances, | ||
1453 | 410 | it is better to run self._parent_database_connection_object.query | ||
1454 | 411 | explicitly. | ||
1455 | 412 | Intended only to be used for non-updating queries | ||
1456 | 413 | (such as "get functions" rather than "create"). | ||
1457 | 414 | :param prog_name: name of the program the result object will reflect | ||
1458 | 415 | :param query: verbatim query we wish the server to execute | ||
1459 | 416 | :return: a FunctionQueryResult corresponding to the server's output | ||
1460 | 417 | """ | ||
1461 | 418 | rest_output = self._db.query(query, returns=client.Node) | ||
1462 | 419 | |||
1463 | 420 | return FunctionQueryResult(parent_db=self._db, | ||
1464 | 421 | program_name=prog_name, | ||
1465 | 422 | rest_output=rest_output) | ||
1466 | 423 | |||
1467 | 424 | def get_program_names(self): | ||
1468 | 425 | """ | ||
1469 | 426 | Execute query to retrieve a list of all programs in the database. | ||
1470 | 427 | Any name in this list can be used verbatim in any SextantConnection | ||
1471 | 428 | method which requires a program-name input. | ||
1472 | 429 | :return: a list of function-name strings. | ||
1473 | 430 | """ | ||
1474 | 431 | q = """MATCH (n) WHERE n.type = "program" RETURN n.name""" | ||
1475 | 432 | program_names = self._db.query(q, returns=str).elements | ||
1476 | 433 | |||
1477 | 434 | result = [el[0] for el in program_names] | ||
1478 | 435 | |||
1479 | 436 | return set(result) | ||
1480 | 437 | |||
1481 | 438 | def check_program_exists(self, program_name): | ||
1482 | 439 | """ | ||
1483 | 440 | Execute query to check whether a program with the given name exists. | ||
1484 | 441 | Returns False if the program_name fails validation against Validator. | ||
1485 | 442 | :return: bool(the program exists in the database). | ||
1486 | 443 | """ | ||
1487 | 444 | |||
1488 | 445 | if not Validator.validate(program_name): | ||
1489 | 446 | return False | ||
1490 | 447 | |||
1491 | 448 | q = ("MATCH (base) WHERE base.name = '{}' AND base.type = 'program' " | ||
1492 | 449 | "RETURN count(base)").format(program_name) | ||
1493 | 450 | |||
1494 | 451 | result = self._db.query(q, returns=int) | ||
1495 | 452 | return result.elements[0][0] > 0 | ||
1496 | 453 | |||
1497 | 454 | def check_function_exists(self, program_name, function_name): | ||
1498 | 455 | """ | ||
1499 | 456 | Execute query to check whether a function with the given name exists. | ||
1500 | 457 | We only check for functions which are children of a program with the | ||
1501 | 458 | given program_name. | ||
1502 | 459 | :param program_name: string name of the program within which to check | ||
1503 | 460 | :param function_name: string name of the function to check for existence | ||
1504 | 461 | :return: bool(names validate correctly, and function exists in program) | ||
1505 | 462 | """ | ||
1506 | 463 | if not self.check_program_exists(program_name): | ||
1507 | 464 | return False | ||
1508 | 465 | |||
1509 | 466 | if not Validator.validate(program_name): | ||
1510 | 467 | return False | ||
1511 | 468 | |||
1512 | 469 | q = ("MATCH (base) WHERE base.name = '{}' AND base.type = 'program'" | ||
1513 | 470 | "MATCH (base)-[r:subject]->(m) WHERE m.name = '{}'" | ||
1514 | 471 | "RETURN count(m)").format(program_name, function_name) | ||
1515 | 472 | |||
1516 | 473 | result = self._db.query(q, returns=int) | ||
1517 | 474 | return result.elements[0][0] > 0 | ||
1518 | 475 | |||
1519 | 476 | def get_function_names(self, program_name): | ||
1520 | 477 | """ | ||
1521 | 478 | Execute query to retrieve a list of all functions in the program. | ||
1522 | 479 | Any of the output names can be used verbatim in any SextantConnection | ||
1523 | 480 | method which requires a function-name input. | ||
1524 | 481 | :param program_name: name of the program whose functions to retrieve | ||
1525 | 482 | :return: None if program_name doesn't exist in the remote database, | ||
1526 | 483 | a set of function-name strings otherwise. | ||
1527 | 484 | """ | ||
1528 | 485 | |||
1529 | 486 | if not self.check_program_exists(program_name): | ||
1530 | 487 | return None | ||
1531 | 488 | |||
1532 | 489 | q = ("MATCH (base) WHERE base.name = '{}' AND base.type = 'program' " | ||
1533 | 490 | "MATCH (base)-[r:subject]->(m) " | ||
1534 | 491 | "RETURN m.name").format(program_name) | ||
1535 | 492 | return {func[0] for func in self._db.query(q)} | ||
1536 | 493 | |||
1537 | 494 | def get_all_functions_called(self, program_name, function_calling): | ||
1538 | 495 | """ | ||
1539 | 496 | Execute query to find all functions called by a function (indirectly). | ||
1540 | 497 | If the given function is not present in the program, returns None; | ||
1541 | 498 | likewise if the program_name does not exist. | ||
1542 | 499 | :param program_name: a string name of the program we wish to query under | ||
1543 | 500 | :param function_calling: string name of a function whose children to find | ||
1544 | 501 | :return: FunctionQueryResult, maximal subgraph rooted at function_calling | ||
1545 | 502 | """ | ||
1546 | 503 | |||
1547 | 504 | if not self.check_program_exists(program_name): | ||
1548 | 505 | return None | ||
1549 | 506 | |||
1550 | 507 | if not self.check_function_exists(program_name, function_calling): | ||
1551 | 508 | return None | ||
1552 | 509 | |||
1553 | 510 | q = """MATCH (base) WHERE base.name = '{}' ANd base.type = 'program' | ||
1554 | 511 | MATCH (base)-[:subject]->(m) WHERE m.name='{}' | ||
1555 | 512 | MATCH (m)-[:calls*]->(n) | ||
1556 | 513 | RETURN distinct n, m""".format(program_name, function_calling) | ||
1557 | 514 | |||
1558 | 515 | return self._execute_query(program_name, q) | ||
1559 | 516 | |||
1560 | 517 | def get_all_functions_calling(self, program_name, function_called): | ||
1561 | 518 | """ | ||
1562 | 519 | Execute query to find all functions which call a function (indirectly). | ||
1563 | 520 | If the given function is not present in the program, returns None; | ||
1564 | 521 | likewise if the program_name does not exist. | ||
1565 | 522 | :param program_name: a string name of the program we wish to query | ||
1566 | 523 | :param function_called: string name of a function whose parents to find | ||
1567 | 524 | :return: FunctionQueryResult, maximal connected subgraph with leaf function_called | ||
1568 | 525 | """ | ||
1569 | 526 | |||
1570 | 527 | if not self.check_program_exists(program_name): | ||
1571 | 528 | return None | ||
1572 | 529 | |||
1573 | 530 | if not self.check_function_exists(program_name, function_called): | ||
1574 | 531 | return None | ||
1575 | 532 | |||
1576 | 533 | q = """MATCH (base) WHERE base.name = '{}' AND base.type = 'program' | ||
1577 | 534 | MATCH (base)-[r:subject]->(m) WHERE m.name='{}' | ||
1578 | 535 | MATCH (n)-[:calls*]->(m) WHERE n.name <> '{}' | ||
1579 | 536 | RETURN distinct n , m""" | ||
1580 | 537 | q = q.format(program_name, function_called, program_name) | ||
1581 | 538 | |||
1582 | 539 | return self._execute_query(program_name, q) | ||
1583 | 540 | |||
1584 | 541 | def get_call_paths(self, program_name, function_calling, function_called): | ||
1585 | 542 | """ | ||
1586 | 543 | Execute query to find all possible routes between two specific nodes. | ||
1587 | 544 | If the given functions are not present in the program, returns None; | ||
1588 | 545 | ditto if the program_name does not exist. | ||
1589 | 546 | :param program_name: string program name | ||
1590 | 547 | :param function_calling: string | ||
1591 | 548 | :param function_called: string | ||
1592 | 549 | :return: FunctionQueryResult, the union of all subgraphs reachable by | ||
1593 | 550 | adding a source at function_calling and a sink at function_called. | ||
1594 | 551 | """ | ||
1595 | 552 | |||
1596 | 553 | if not self.check_program_exists(program_name): | ||
1597 | 554 | return None | ||
1598 | 555 | |||
1599 | 556 | if not self.check_function_exists(program_name, function_called): | ||
1600 | 557 | return None | ||
1601 | 558 | |||
1602 | 559 | if not self.check_function_exists(program_name, function_calling): | ||
1603 | 560 | return None | ||
1604 | 561 | |||
1605 | 562 | q = r"""MATCH (pr) WHERE pr.name = '{}' AND pr.type = 'program' | ||
1606 | 563 | MATCH p=(start {{name: "{}" }})-[:calls*]->(end {{name:"{}"}}) | ||
1607 | 564 | WHERE (pr)-[:subject]->(start) | ||
1608 | 565 | WITH DISTINCT nodes(p) AS result | ||
1609 | 566 | UNWIND result AS answer | ||
1610 | 567 | RETURN answer""" | ||
1611 | 568 | q = q.format(program_name, function_calling, function_called) | ||
1612 | 569 | |||
1613 | 570 | return self._execute_query(program_name, q) | ||
1614 | 571 | |||
1615 | 572 | def get_whole_program(self, program_name): | ||
1616 | 573 | """Execute query to find the entire program with a given name. | ||
1617 | 574 | If the program is not present in the remote database, returns None. | ||
1618 | 575 | :param: program_name: a string name of the program we wish to return. | ||
1619 | 576 | :return: a FunctionQueryResult consisting of the program graph. | ||
1620 | 577 | """ | ||
1621 | 578 | |||
1622 | 579 | if not self.check_program_exists(program_name): | ||
1623 | 580 | return None | ||
1624 | 581 | |||
1625 | 582 | query = """MATCH (base) WHERE base.name = '{}' AND base.type = 'program' | ||
1626 | 583 | MATCH (base)-[subject:subject]->(m) | ||
1627 | 584 | RETURN DISTINCT (m)""".format(program_name) | ||
1628 | 585 | |||
1629 | 586 | return self._execute_query(program_name, query) | ||
1630 | 587 | |||
1631 | 588 | def get_shortest_path_between_functions(self, program_name, func1, func2): | ||
1632 | 589 | """ | ||
1633 | 590 | Execute query to get a single, shortest, path between two functions. | ||
1634 | 591 | :param program_name: string name of the program we wish to search under | ||
1635 | 592 | :param func1: the name of the originating function of our shortest path | ||
1636 | 593 | :param func2: the name of the function at which to terminate the path | ||
1637 | 594 | :return: FunctionQueryResult shortest path between func1 and func2. | ||
1638 | 595 | """ | ||
1639 | 596 | if not self.check_program_exists(program_name): | ||
1640 | 597 | return None | ||
1641 | 598 | |||
1642 | 599 | if not self.check_function_exists(program_name, func1): | ||
1643 | 600 | return None | ||
1644 | 601 | |||
1645 | 602 | if not self.check_function_exists(program_name, func2): | ||
1646 | 603 | return None | ||
1647 | 604 | |||
1648 | 605 | q = """MATCH (func1 {{ name:"{}" }}),(func2 {{ name:"{}" }}), | ||
1649 | 606 | p = shortestPath((func1)-[:calls*]->(func2)) | ||
1650 | 607 | UNWIND nodes(p) AS ans | ||
1651 | 608 | RETURN ans""".format(func1, func2) | ||
1652 | 609 | |||
1653 | 610 | return self._execute_query(program_name, q) | ||
1654 | 0 | 611 | ||
1655 | === added file 'src/sextant/errors.py' | |||
1656 | --- src/sextant/errors.py 1970-01-01 00:00:00 +0000 | |||
1657 | +++ src/sextant/errors.py 2014-08-15 12:04:08 +0000 | |||
1658 | @@ -0,0 +1,20 @@ | |||
1659 | 1 | # ----------------------------------------------------------------------------- | ||
1660 | 2 | # errors.py -- Sextant error definitions | ||
1661 | 3 | # | ||
1662 | 4 | # August 2014, Phil Connell | ||
1663 | 5 | # | ||
1664 | 6 | # Copyright 2014, Ensoft Ltd. | ||
1665 | 7 | # ----------------------------------------------------------------------------- | ||
1666 | 8 | |||
1667 | 9 | from __future__ import absolute_import, print_function | ||
1668 | 10 | |||
1669 | 11 | __all__ = ( | ||
1670 | 12 | "MissingDependencyError", | ||
1671 | 13 | ) | ||
1672 | 14 | |||
1673 | 15 | |||
1674 | 16 | class MissingDependencyError(Exception): | ||
1675 | 17 | """ | ||
1676 | 18 | Raised if trying an operation for which an optional dependency is missing. | ||
1677 | 19 | """ | ||
1678 | 20 | |||
1679 | 0 | 21 | ||
1680 | === added file 'src/sextant/export.py' | |||
1681 | --- src/sextant/export.py 1970-01-01 00:00:00 +0000 | |||
1682 | +++ src/sextant/export.py 2014-08-15 12:04:08 +0000 | |||
1683 | @@ -0,0 +1,152 @@ | |||
1684 | 1 | # ----------------------------------------- | ||
1685 | 2 | # Sextant | ||
1686 | 3 | # Copyright 2014, Ensoft Ltd. | ||
1687 | 4 | # Author: Patrick Stevens, James Harkin | ||
1688 | 5 | # ----------------------------------------- | ||
1689 | 6 | # Convert a program from internal Python representation to various output formats | ||
1690 | 7 | |||
1691 | 8 | __all__ = "ProgramConverter" | ||
1692 | 9 | |||
1693 | 10 | |||
1694 | 11 | class ProgramConverter: | ||
1695 | 12 | # Given our internal program representation, converts it to output formats. | ||
1696 | 13 | # Currently supported: GraphML | ||
1697 | 14 | |||
1698 | 15 | @staticmethod | ||
1699 | 16 | def get_supported_outputs(): | ||
1700 | 17 | return ['graphml', 'yed_graphml', 'dot'] | ||
1701 | 18 | |||
1702 | 19 | @staticmethod | ||
1703 | 20 | def get_display_name(function, suppress_common_nodes=False): | ||
1704 | 21 | """ | ||
1705 | 22 | Given a Function object, retrieve the label we attach to it. | ||
1706 | 23 | For instance, function-pointers are labelled "(function pointer)", | ||
1707 | 24 | while the `main` function is usually labelled "main". | ||
1708 | 25 | """ | ||
1709 | 26 | if function.type == "function_pointer": | ||
1710 | 27 | name = "(function pointer)" | ||
1711 | 28 | else: | ||
1712 | 29 | name = function.name | ||
1713 | 30 | |||
1714 | 31 | if suppress_common_nodes and function.is_common: | ||
1715 | 32 | name += ' (common)' | ||
1716 | 33 | return name | ||
1717 | 34 | |||
1718 | 35 | @staticmethod | ||
1719 | 36 | def to_dot(program, suppress_common_nodes=False): | ||
1720 | 37 | """ | ||
1721 | 38 | Convert the program to DOT output format. | ||
1722 | 39 | """ | ||
1723 | 40 | output_str = 'digraph "{}" {{\n '.format(program.program_name) | ||
1724 | 41 | output_str += 'overlap=false; \n' | ||
1725 | 42 | |||
1726 | 43 | font_name = "helvetica" | ||
1727 | 44 | |||
1728 | 45 | for func in program.get_functions(): | ||
1729 | 46 | if func.type == "plt_stub": | ||
1730 | 47 | output_str += ' "{}" [fillcolor=pink, style=filled]\n'.format(func.name) | ||
1731 | 48 | elif func.type == "function_pointer": | ||
1732 | 49 | output_str += ' "{}" [fillcolor=yellow, style=filled]\n'.format(func.name) | ||
1733 | 50 | |||
1734 | 51 | # in all cases, even if we've specified that we want a filled-in | ||
1735 | 52 | # node already, DOT lets us add more information about that node | ||
1736 | 53 | # so we can insist on turning that same node into a box-shape | ||
1737 | 54 | # and changing its font. | ||
1738 | 55 | output_str += ' "{}" [label="{}", fontname="{}", shape=box]\n'.format(func.name, | ||
1739 | 56 | ProgramConverter.get_display_name(func, suppress_common_nodes), | ||
1740 | 57 | font_name) | ||
1741 | 58 | if func.is_common: | ||
1742 | 59 | output_str += ' "{}" [fillcolor=lightgreen, style=filled]\n'.format(func.name) | ||
1743 | 60 | |||
1744 | 61 | for func_called in func.functions_i_call: | ||
1745 | 62 | if not (suppress_common_nodes and func_called.is_common): | ||
1746 | 63 | output_str += ' "{}" -> "{}"\n'.format(func.name, func_called.name) | ||
1747 | 64 | |||
1748 | 65 | output_str += '}' | ||
1749 | 66 | return output_str | ||
1750 | 67 | |||
1751 | 68 | |||
1752 | 69 | |||
1753 | 70 | @staticmethod | ||
1754 | 71 | def to_yed_graphml(program, suppress_common_nodes=False): | ||
1755 | 72 | commonly_called = [] | ||
1756 | 73 | output_str = """<?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||
1757 | 74 | <graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" \ | ||
1758 | 75 | xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" \ | ||
1759 | 76 | xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd"> | ||
1760 | 77 | """ | ||
1761 | 78 | output_str += """<key for="graphml" id="d0" yfiles.type="resources"/> | ||
1762 | 79 | <key for="port" id="d1" yfiles.type="portgraphics"/> | ||
1763 | 80 | <key for="port" id="d2" yfiles.type="portgeometry"/> | ||
1764 | 81 | <key for="port" id="d3" yfiles.type="portuserdata"/> | ||
1765 | 82 | <key attr.name="url" attr.type="string" for="node" id="d4"/> | ||
1766 | 83 | <key attr.name="description" attr.type="string" for="node" id="d5"/> | ||
1767 | 84 | <key for="node" id="d6" yfiles.type="nodegraphics"/> | ||
1768 | 85 | <key attr.name="url" attr.type="string" for="edge" id="d7"/> | ||
1769 | 86 | <key attr.name="description" attr.type="string" for="edge" id="d8"/> | ||
1770 | 87 | <key for="edge" id="d9" yfiles.type="edgegraphics"/>\n""" | ||
1771 | 88 | |||
1772 | 89 | output_str += """<graph id="{}" edgedefault="directed">\n""".format(program.program_name) | ||
1773 | 90 | |||
1774 | 91 | for func in program.get_functions(): | ||
1775 | 92 | display_func = ProgramConverter.get_display_name(func) | ||
1776 | 93 | if func.type == "plt_stub": | ||
1777 | 94 | colour = "#ff00ff" | ||
1778 | 95 | elif func.type == "function_pointer": | ||
1779 | 96 | colour = "#99ffff" | ||
1780 | 97 | elif func.is_common: | ||
1781 | 98 | colour = "#00FF00" | ||
1782 | 99 | else: | ||
1783 | 100 | colour = "#ffcc00" | ||
1784 | 101 | output_str += """<node id="{}"> | ||
1785 | 102 | <data key="d6"> | ||
1786 | 103 | <y:ShapeNode> | ||
1787 | 104 | <y:Geometry height="{}" width="{}" x="60.0" y="0.0"/> | ||
1788 | 105 | <y:Fill color="{}" transparent="false"/> | ||
1789 | 106 | <y:BorderStyle color="#000000" type="line" width="1.0"/> | ||
1790 | 107 | <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" | ||
1791 | 108 | fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" | ||
1792 | 109 | height="18.701171875" modelName="custom" textColor="#000000" visible="true" | ||
1793 | 110 | width="36.6953125" x="-3.34765625" y="5.6494140625">{}<y:LabelModel> | ||
1794 | 111 | <y:SmartNodeLabelModel distance="4.0"/> | ||
1795 | 112 | </y:LabelModel> | ||
1796 | 113 | <y:ModelParameter> | ||
1797 | 114 | <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" | ||
1798 | 115 | nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" | ||
1799 | 116 | upY="-1.0"/> | ||
1800 | 117 | </y:ModelParameter> | ||
1801 | 118 | </y:NodeLabel> | ||
1802 | 119 | <y:Shape type="rectangle"/> | ||
1803 | 120 | </y:ShapeNode> | ||
1804 | 121 | </data> | ||
1805 | 122 | </node>\n""".format(func.name, 20, len(display_func)*8, colour, display_func) | ||
1806 | 123 | for callee in func.functions_i_call: | ||
1807 | 124 | if callee not in commonly_called: | ||
1808 | 125 | if not(suppress_common_nodes and callee.is_common): | ||
1809 | 126 | output_str += """<edge source="{}" target="{}"> <data key="d9"> | ||
1810 | 127 | <y:PolyLineEdge> | ||
1811 | 128 | <y:LineStyle color="#000000" type="line" width="1.0"/> | ||
1812 | 129 | <y:Arrows source="none" target="standard"/> | ||
1813 | 130 | <y:BendStyle smoothed="false"/> | ||
1814 | 131 | </y:PolyLineEdge> | ||
1815 | 132 | </data> | ||
1816 | 133 | </edge>\n""".format(func.name, callee.name) | ||
1817 | 134 | |||
1818 | 135 | output_str += '</graph>\n<data key="d0"> <y:Resources/> </data>\n</graphml>' | ||
1819 | 136 | return output_str | ||
1820 | 137 | |||
1821 | 138 | @staticmethod | ||
1822 | 139 | def to_graphml(program): | ||
1823 | 140 | output_str = """<?xml version="1.0" encoding="UTF-8"?> | ||
1824 | 141 | <graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" \ | ||
1825 | 142 | xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd"> | ||
1826 | 143 | """ | ||
1827 | 144 | output_str += '<graph id="{}" edgedefault="directed">\n'.format(program.program_name) | ||
1828 | 145 | |||
1829 | 146 | for func in program.get_functions(): | ||
1830 | 147 | output_str += '<node id="{}"> <data key="name">{}</data> </node>\n'.format(func.name, ProgramConverter.get_display_name(func)) | ||
1831 | 148 | for callee in func.functions_i_call: | ||
1832 | 149 | output_str += '<edge source="{}" target="{}"> <data key="calls">1</data> </edge>\n'.format(func.name, callee.name) | ||
1833 | 150 | |||
1834 | 151 | output_str += '</graph>\n</graphml>' | ||
1835 | 152 | return output_str | ||
1836 | 0 | \ No newline at end of file | 153 | \ No newline at end of file |
1837 | 1 | 154 | ||
1838 | === added file 'src/sextant/objdump_parser.py' | |||
1839 | --- src/sextant/objdump_parser.py 1970-01-01 00:00:00 +0000 | |||
1840 | +++ src/sextant/objdump_parser.py 2014-08-15 12:04:08 +0000 | |||
1841 | @@ -0,0 +1,272 @@ | |||
1842 | 1 | # ----------------------------------------- | ||
1843 | 2 | # Sextant | ||
1844 | 3 | # Copyright 2014, Ensoft Ltd. | ||
1845 | 4 | # Author: Patrick Stevens | ||
1846 | 5 | # ----------------------------------------- | ||
1847 | 6 | |||
1848 | 7 | #!/usr/bin/python3 | ||
1849 | 8 | |||
1850 | 9 | import re | ||
1851 | 10 | import argparse | ||
1852 | 11 | import os.path | ||
1853 | 12 | import subprocess | ||
1854 | 13 | import logging | ||
1855 | 14 | |||
1856 | 15 | |||
1857 | 16 | class ParsedObject(): | ||
1858 | 17 | """ | ||
1859 | 18 | Represents a function as parsed from an objdump disassembly. | ||
1860 | 19 | Has a name (which is the verbatim name like '__libc_start_main@plt'), | ||
1861 | 20 | a position (which is the virtual memory location in hex, like '08048320' | ||
1862 | 21 | extracted from the dump), | ||
1863 | 22 | and a canonical_position (which is the virtual memory location in hex | ||
1864 | 23 | but stripped of leading 0s, so it should be a | ||
1865 | 24 | unique id). | ||
1866 | 25 | It also has a list what_do_i_call of ParsedObjects it calls using the | ||
1867 | 26 | assembly keyword 'call'. | ||
1868 | 27 | It has a list original_code of its assembler code, too, in case it's useful. | ||
1869 | 28 | """ | ||
1870 | 29 | |||
1871 | 30 | @staticmethod | ||
1872 | 31 | def get_canonical_position(position): | ||
1873 | 32 | return position.lstrip('0') | ||
1874 | 33 | |||
1875 | 34 | def __eq__(self, other): | ||
1876 | 35 | return self.name == other.name | ||
1877 | 36 | |||
1878 | 37 | def __init__(self, input_lines=None, assembler_section='', function_name='', | ||
1879 | 38 | ignore_function_pointers=True, function_pointer_id=None): | ||
1880 | 39 | """ | ||
1881 | 40 | Create a new ParsedObject given the definition-lines from objdump -S. | ||
1882 | 41 | A sample first definition-line is '08048300 <__gmon_start__@plt>:\n' | ||
1883 | 42 | but this method | ||
1884 | 43 | expects to see the entire definition eg | ||
1885 | 44 | |||
1886 | 45 | 080482f0 <puts@plt>: | ||
1887 | 46 | 80482f0: ff 25 00 a0 04 08 jmp *0x804a000 | ||
1888 | 47 | 80482f6: 68 00 00 00 00 push $0x0 | ||
1889 | 48 | 80482fb: e9 e0 ff ff ff jmp 80482e0 <_init+0x30> | ||
1890 | 49 | |||
1891 | 50 | We also might expect assembler_section, which is for instance '.init' | ||
1892 | 51 | in 'Disassembly of section .init:' | ||
1893 | 52 | function_name is used if we want to give this function a custom name. | ||
1894 | 53 | ignore_function_pointers=True will pretend that calls to (eg) *eax do | ||
1895 | 54 | not exist; setting to False makes us create stubs for those calls. | ||
1896 | 55 | function_pointer_id is only used internally; it refers to labelling | ||
1897 | 56 | of function pointers if ignore_function_pointers is False. Each | ||
1898 | 57 | stub is given a unique numeric ID: this parameter tells init where | ||
1899 | 58 | to start counting these IDs from. | ||
1900 | 59 | |||
1901 | 60 | """ | ||
1902 | 61 | if input_lines is None: | ||
1903 | 62 | # get around Python's inability to pass in empty lists by value | ||
1904 | 63 | input_lines = [] | ||
1905 | 64 | |||
1906 | 65 | self.name = function_name or re.search(r'<.+>', input_lines[0]).group(0).strip('<>') | ||
1907 | 66 | self.what_do_i_call = [] | ||
1908 | 67 | self.position = '' | ||
1909 | 68 | |||
1910 | 69 | if input_lines: | ||
1911 | 70 | self.position = re.search(r'^[0-9a-f]+', input_lines[0]).group(0) | ||
1912 | 71 | self.canonical_position = ParsedObject.get_canonical_position(self.position) | ||
1913 | 72 | self.assembler_section = assembler_section | ||
1914 | 73 | self.original_code = input_lines[1:] | ||
1915 | 74 | |||
1916 | 75 | call_regex_compiled = (ignore_function_pointers and re.compile(r'\tcall. +[^\*]+\n')) or re.compile(r'\tcall. +.+\n') | ||
1917 | 76 | |||
1918 | 77 | lines_where_i_call = [line for line in input_lines if call_regex_compiled.search(line)] | ||
1919 | 78 | |||
1920 | 79 | if not ignore_function_pointers and not function_pointer_id: | ||
1921 | 80 | function_pointer_id = [1] | ||
1922 | 81 | |||
1923 | 82 | for line in lines_where_i_call: | ||
1924 | 83 | # we'll catch call and callq for the moment | ||
1925 | 84 | called = (call_regex_compiled.search(line).group(0))[8:].lstrip(' ').rstrip('\n') | ||
1926 | 85 | if called[0] == '*' and ignore_function_pointers == False: | ||
1927 | 86 | # we have a function pointer, which we'll want to give a distinct name | ||
1928 | 87 | address = '0' | ||
1929 | 88 | name = '_._function_pointer_' + str(function_pointer_id[0]) | ||
1930 | 89 | function_pointer_id[0] += 1 | ||
1931 | 90 | |||
1932 | 91 | self.what_do_i_call.append((address, name)) | ||
1933 | 92 | |||
1934 | 93 | else: # we're not on a function pointer | ||
1935 | 94 | called_split = called.split(' ') | ||
1936 | 95 | if len(called_split) == 2: | ||
1937 | 96 | address, name = called_split | ||
1938 | 97 | name = name.strip('<>') | ||
1939 | 98 | # we still want to remove address offsets like +0x09 from the end of name | ||
1940 | 99 | match = re.match(r'^.+(?=\+0x[a-f0-9]+$)', name) | ||
1941 | 100 | if match is not None: | ||
1942 | 101 | name = match.group(0) | ||
1943 | 102 | self.what_do_i_call.append((address, name.strip('<>'))) | ||
1944 | 103 | else: # the format of the "what do i call" is not recognised as a name/address pair | ||
1945 | 104 | self.what_do_i_call.append(tuple(called_split)) | ||
1946 | 105 | |||
1947 | 106 | def __str__(self): | ||
1948 | 107 | if self.position: | ||
1949 | 108 | return 'Memory address ' + self.position + ' with name ' + self.name + ' in section ' + str( | ||
1950 | 109 | self.assembler_section) | ||
1951 | 110 | else: | ||
1952 | 111 | return 'Name ' + self.name | ||
1953 | 112 | |||
1954 | 113 | def __repr__(self): | ||
1955 | 114 | out_str = 'Disassembly of section ' + self.assembler_section + ':\n\n' + self.position + ' ' + self.name + ':\n' | ||
1956 | 115 | return out_str + '\n'.join([' ' + line for line in self.original_code]) | ||
1957 | 116 | |||
1958 | 117 | |||
1959 | 118 | class Parser: | ||
1960 | 119 | # Class to manipulate the output of objdump | ||
1961 | 120 | |||
1962 | 121 | def __init__(self, input_file_location='', file_contents=None, sections_to_view=None, ignore_function_pointers=False): | ||
1963 | 122 | """Creates a new Parser, given an input file path. That path should be an output from objdump -D. | ||
1964 | 123 | Alternatively, supply file_contents, as a list of each line of the objdump output. We expect newlines | ||
1965 | 124 | to have been stripped from the end of each of these lines. | ||
1966 | 125 | sections_to_view makes sure we only use the specified sections (use [] for 'all sections' and None for none). | ||
1967 | 126 | """ | ||
1968 | 127 | if file_contents is None: | ||
1969 | 128 | file_contents = [] | ||
1970 | 129 | |||
1971 | 130 | if sections_to_view is None: | ||
1972 | 131 | sections_to_view = [] | ||
1973 | 132 | |||
1974 | 133 | if input_file_location: | ||
1975 | 134 | file_to_read = open(input_file_location, 'r') | ||
1976 | 135 | self.source_string_list = [line for line in file_to_read] | ||
1977 | 136 | file_to_read.close() | ||
1978 | 137 | elif file_contents: | ||
1979 | 138 | self.source_string_list = [string + '\n' for string in file_contents] | ||
1980 | 139 | self.parsed_objects = [] | ||
1981 | 140 | self.sections_to_view = sections_to_view | ||
1982 | 141 | self.ignore_function_pointers = ignore_function_pointers | ||
1983 | 142 | self.pointer_identifier = [1] | ||
1984 | 143 | |||
1985 | 144 | def create_objects(self): | ||
1986 | 145 | """ Go through the source_string_list, getting object names (like 'main') along with the corresponding | ||
1987 | 146 | definitions, and put them into parsed_objects """ | ||
1988 | 147 | if self.sections_to_view is None: | ||
1989 | 148 | return | ||
1990 | 149 | |||
1991 | 150 | is_in_section = lambda name: self.sections_to_view == [] or name in self.sections_to_view | ||
1992 | 151 | |||
1993 | 152 | parsed_objects = [] | ||
1994 | 153 | current_object = [] | ||
1995 | 154 | current_section = '' | ||
1996 | 155 | regex_compiled_addr_and_name = re.compile(r'[0-9a-f]+ <.+>:\n') | ||
1997 | 156 | regex_compiled_section = re.compile(r'section .+:\n') | ||
1998 | 157 | |||
1999 | 158 | for line in self.source_string_list[4:]: # we bodge, since the file starts with a little bit of guff | ||
2000 | 159 | if regex_compiled_addr_and_name.match(line): | ||
2001 | 160 | # we are a starting line | ||
2002 | 161 | current_object = [line] | ||
2003 | 162 | elif re.match(r'Disassembly of section', line): | ||
2004 | 163 | current_section = regex_compiled_section.search(line).group(0).lstrip('section ').rstrip(':\n') | ||
2005 | 164 | current_object = [] | ||
2006 | 165 | elif line == '\n': | ||
2007 | 166 | # we now need to stop parsing the current block, and store it | ||
2008 | 167 | if len(current_object) > 0 and is_in_section(current_section): | ||
2009 | 168 | parsed_objects.append(ParsedObject(input_lines=current_object, assembler_section=current_section, | ||
2010 | 169 | ignore_function_pointers=self.ignore_function_pointers, | ||
2011 | 170 | function_pointer_id=self.pointer_identifier)) | ||
2012 | 171 | else: | ||
2013 | 172 | current_object.append(line) | ||
2014 | 173 | |||
2015 | 174 | # now we should be done. We assumed that blocks begin with r'[0-9a-f]+ <.+>:\n' and end with a newline. | ||
2016 | 175 | # clear duplicates: | ||
2017 | 176 | |||
2018 | 177 | self.parsed_objects = [] | ||
2019 | 178 | for obj in parsed_objects: | ||
2020 | 179 | if obj not in self.parsed_objects: # this is so that if we jump into the function at an offset, | ||
2021 | 180 | # we still register it as being the old function, not some new function at a different address | ||
2022 | 181 | # with the same name | ||
2023 | 182 | self.parsed_objects.append(obj) | ||
2024 | 183 | |||
2025 | 184 | # by this point, each object contains a self.what_do_i_call which is a list of tuples | ||
2026 | 185 | # ('address', 'name') if the address and name were recognised, or else (thing1, thing2, ...) | ||
2027 | 186 | # where the instruction was call thing1 thing2 thing3... . | ||
2028 | 187 | |||
2029 | 188 | def object_lookup(self, object_name='', object_address=''): | ||
2030 | 189 | """Returns the object with name object_name or address object_address (at least one must be given). | ||
2031 | 190 | If objects with the given name or address | ||
2032 | 191 | are not found, returns None.""" | ||
2033 | 192 | |||
2034 | 193 | if object_name == '' and object_address == '': | ||
2035 | 194 | return None | ||
2036 | 195 | |||
2037 | 196 | trial_obj = self.parsed_objects | ||
2038 | 197 | |||
2039 | 198 | if object_name != '': | ||
2040 | 199 | trial_obj = [obj for obj in trial_obj if obj.name == object_name] | ||
2041 | 200 | |||
2042 | 201 | if object_address != '': | ||
2043 | 202 | trial_obj = [obj for obj in trial_obj if | ||
2044 | 203 | obj.canonical_position == ParsedObject.get_canonical_position(object_address)] | ||
2045 | 204 | |||
2046 | 205 | if len(trial_obj) == 0: | ||
2047 | 206 | return None | ||
2048 | 207 | |||
2049 | 208 | return trial_obj | ||
2050 | 209 | |||
2051 | 210 | def get_parsed_objects(filepath, sections_to_view, not_object_file, readable=False, ignore_function_pointers=False): | ||
2052 | 211 | if sections_to_view is None: | ||
2053 | 212 | sections_to_view = [] # because we use None for "no sections"; the intent of not providing any sections | ||
2054 | 213 | # on the command line was to look at all sections, not none | ||
2055 | 214 | |||
2056 | 215 | # first, check whether the given file exists | ||
2057 | 216 | if not os.path.isfile(filepath): | ||
2058 | 217 | logging.error('Input file does not exist') | ||
2059 | 218 | return False | ||
2060 | 219 | |||
2061 | 220 | #now the file should exist | ||
2062 | 221 | if not not_object_file: #if it is something we need to run through objdump first | ||
2063 | 222 | #we need first to run the object file through objdump | ||
2064 | 223 | |||
2065 | 224 | objdump_file_contents = subprocess.check_output(['objdump', '-D', filepath]) | ||
2066 | 225 | objdump_str = objdump_file_contents.decode('utf-8') | ||
2067 | 226 | |||
2068 | 227 | p = Parser(file_contents=objdump_str.split('\n'), sections_to_view=sections_to_view, ignore_function_pointers=ignore_function_pointers) | ||
2069 | 228 | else: | ||
2070 | 229 | try: | ||
2071 | 230 | p = Parser(input_file_location=filepath, sections_to_view=sections_to_view, ignore_function_pointers=ignore_function_pointers) | ||
2072 | 231 | except UnicodeDecodeError: | ||
2073 | 232 | logging.error('File could not be parsed as a string. Did you mean to supply --object-file?') | ||
2074 | 233 | return False | ||
2075 | 234 | |||
2076 | 235 | if readable: # if we're being called from the command line | ||
2077 | 236 | print('File read; beginning parse.') | ||
2078 | 237 | #file is now read, and we start parsing | ||
2079 | 238 | |||
2080 | 239 | p.create_objects() | ||
2081 | 240 | return p.parsed_objects | ||
2082 | 241 | |||
2083 | 242 | def main(): | ||
2084 | 243 | argumentparser = argparse.ArgumentParser(description="Parse the output of objdump.") | ||
2085 | 244 | argumentparser.add_argument('--filepath', metavar="FILEPATH", help="path to input file", type=str, nargs=1) | ||
2086 | 245 | argumentparser.add_argument('--not-object-file', help="import text objdump output instead of the compiled file", default=False, | ||
2087 | 246 | action='store_true') | ||
2088 | 247 | argumentparser.add_argument('--sections-to-view', metavar="SECTIONS", | ||
2089 | 248 | help="sections of disassembly to view, like '.text'; leave blank for 'all'", | ||
2090 | 249 | type=str, nargs='*') | ||
2091 | 250 | argumentparser.add_argument('--ignore-function-pointers', help='whether to skip parsing calls to function pointers', action='store_true', default=False) | ||
2092 | 251 | |||
2093 | 252 | parsed = argumentparser.parse_args() | ||
2094 | 253 | |||
2095 | 254 | filepath = parsed.filepath[0] | ||
2096 | 255 | sections_to_view = parsed.sections_to_view | ||
2097 | 256 | not_object_file = parsed.not_object_file | ||
2098 | 257 | readable = True | ||
2099 | 258 | function_pointers = parsed.ignore_function_pointers | ||
2100 | 259 | |||
2101 | 260 | parsed_objs = get_parsed_objects(filepath, sections_to_view, not_object_file, readable, function_pointers) | ||
2102 | 261 | if parsed_objs is False: | ||
2103 | 262 | return 1 | ||
2104 | 263 | |||
2105 | 264 | if readable: | ||
2106 | 265 | for named_function in parsed_objs: | ||
2107 | 266 | print(named_function.name) | ||
2108 | 267 | print([f[-1] for f in named_function.what_do_i_call]) # use [-1] to get the last element, since: | ||
2109 | 268 | #either we are in ('address', 'name'), when we want the last element, or else we are in (thing1, thing2, ...) | ||
2110 | 269 | #so for the sake of argument we'll take the last thing | ||
2111 | 270 | |||
2112 | 271 | if __name__ == "__main__": | ||
2113 | 272 | main() | ||
2114 | 0 | 273 | ||
2115 | === added file 'src/sextant/pyinput.py' | |||
2116 | --- src/sextant/pyinput.py 1970-01-01 00:00:00 +0000 | |||
2117 | +++ src/sextant/pyinput.py 2014-08-15 12:04:08 +0000 | |||
2118 | @@ -0,0 +1,180 @@ | |||
2119 | 1 | # ----------------------------------------------------------------------------- | ||
2120 | 2 | # pyinput.py -- Input information from Python programs. | ||
2121 | 3 | # | ||
2122 | 4 | # August 2014, Phil Connell | ||
2123 | 5 | # | ||
2124 | 6 | # Copyright 2014, Ensoft Ltd. | ||
2125 | 7 | # ----------------------------------------------------------------------------- | ||
2126 | 8 | |||
2127 | 9 | from __future__ import absolute_import, print_function | ||
2128 | 10 | |||
2129 | 11 | __all__ = ( | ||
2130 | 12 | "trace", | ||
2131 | 13 | ) | ||
2132 | 14 | |||
2133 | 15 | |||
2134 | 16 | import contextlib | ||
2135 | 17 | import sys | ||
2136 | 18 | |||
2137 | 19 | from . import errors | ||
2138 | 20 | from . import db_api | ||
2139 | 21 | |||
2140 | 22 | |||
2141 | 23 | # Optional, should be checked at API entrypoints requiring entrails (and | ||
2142 | 24 | # yes, the handling is a bit fugly). | ||
2143 | 25 | try: | ||
2144 | 26 | import entrails | ||
2145 | 27 | except ImportError: | ||
2146 | 28 | _entrails_available = False | ||
2147 | 29 | class entrails: | ||
2148 | 30 | EntrailsOutput = object | ||
2149 | 31 | else: | ||
2150 | 32 | _entrails_available = True | ||
2151 | 33 | |||
2152 | 34 | |||
2153 | 35 | class _SextantOutput(entrails.EntrailsOutput): | ||
2154 | 36 | """Record calls traced by entrails in a sextant database.""" | ||
2155 | 37 | |||
2156 | 38 | # Internal attributes: | ||
2157 | 39 | # | ||
2158 | 40 | # _conn: | ||
2159 | 41 | # Sextant connection. | ||
2160 | 42 | # _fns: | ||
2161 | 43 | # Stack of function names (implemented as a list), reflecting the current | ||
2162 | 44 | # call stack, based on enter, exception and exit events. | ||
2163 | 45 | # _prog: | ||
2164 | 46 | # Sextant program representation. | ||
2165 | 47 | _conn = None | ||
2166 | 48 | _fns = None | ||
2167 | 49 | _prog = None | ||
2168 | 50 | |||
2169 | 51 | def __init__(self, conn, program_name): | ||
2170 | 52 | """ | ||
2171 | 53 | Initialise this output. | ||
2172 | 54 | |||
2173 | 55 | conn: | ||
2174 | 56 | Connection to the Sextant database. | ||
2175 | 57 | program_name: | ||
2176 | 58 | String used to refer to the traced program in sextant. | ||
2177 | 59 | |||
2178 | 60 | """ | ||
2179 | 61 | self._conn = conn | ||
2180 | 62 | self._fns = [] | ||
2181 | 63 | self._prog = self._conn.new_program(program_name) | ||
2182 | 64 | self._tracer = self._trace() | ||
2183 | 65 | next(self._tracer) | ||
2184 | 66 | |||
2185 | 67 | def _add_frame(self, event): | ||
2186 | 68 | """Add a function call to the internal stack.""" | ||
2187 | 69 | name = event.qualname() | ||
2188 | 70 | self._fns.append(name) | ||
2189 | 71 | self._prog.add_function(name) | ||
2190 | 72 | |||
2191 | 73 | try: | ||
2192 | 74 | prev_name = self._fns[-2] | ||
2193 | 75 | except IndexError: | ||
2194 | 76 | pass | ||
2195 | 77 | else: | ||
2196 | 78 | self._prog.add_function_call(prev_name, name) | ||
2197 | 79 | |||
2198 | 80 | def _remove_frame(self, event): | ||
2199 | 81 | """Remove a function call from the internal stack.""" | ||
2200 | 82 | assert event.qualname() == self._fns[-1], \ | ||
2201 | 83 | "Unexpected event for {}".format(event.qualname()) | ||
2202 | 84 | self._fns.pop() | ||
2203 | 85 | |||
2204 | 86 | def _handle_simple_event(self, what, event): | ||
2205 | 87 | """Handle a single trace event, not needing recursive processing.""" | ||
2206 | 88 | handled = True | ||
2207 | 89 | |||
2208 | 90 | if what == "enter": | ||
2209 | 91 | self._add_frame(event) | ||
2210 | 92 | elif what == "exit": | ||
2211 | 93 | self._remove_frame(event) | ||
2212 | 94 | else: | ||
2213 | 95 | handled = False | ||
2214 | 96 | |||
2215 | 97 | return handled | ||
2216 | 98 | |||
2217 | 99 | def _trace(self): | ||
2218 | 100 | """Coroutine that processes trace events it's sent.""" | ||
2219 | 101 | while True: | ||
2220 | 102 | what, event = yield | ||
2221 | 103 | |||
2222 | 104 | handled = self._handle_simple_event(what, event) | ||
2223 | 105 | if not handled: | ||
2224 | 106 | if what == "exception": | ||
2225 | 107 | # An exception doesn't necessarily mean the current stack | ||
2226 | 108 | # frame is exiting. Need to check whether the next event is | ||
2227 | 109 | # an exception in a different stack frame, implying that | ||
2228 | 110 | # the exception is propagating up the stack. | ||
2229 | 111 | while True: | ||
2230 | 112 | prev_event = event | ||
2231 | 113 | prev_name = event.qualname() | ||
2232 | 114 | what, event = yield | ||
2233 | 115 | if event == "exception": | ||
2234 | 116 | if event.qualname() != prev_name: | ||
2235 | 117 | self._remove_frame(prev_event) | ||
2236 | 118 | else: | ||
2237 | 119 | handled = self._handle_simple_event(what, event) | ||
2238 | 120 | assert handled | ||
2239 | 121 | break | ||
2240 | 122 | |||
2241 | 123 | else: | ||
2242 | 124 | raise NotImplementedError | ||
2243 | 125 | |||
2244 | 126 | def close(self): | ||
2245 | 127 | self._prog.commit() | ||
2246 | 128 | |||
2247 | 129 | def enter(self, event): | ||
2248 | 130 | self._tracer.send(("enter", event)) | ||
2249 | 131 | |||
2250 | 132 | def exception(self, event): | ||
2251 | 133 | self._tracer.send(("exception", event)) | ||
2252 | 134 | |||
2253 | 135 | def exit(self, event): | ||
2254 | 136 | self._tracer.send(("exit", event)) | ||
2255 | 137 | |||
2256 | 138 | |||
2257 | 139 | # @@@ config parsing shouldn't be done in __main__ (we want to get the neo4j | ||
2258 | 140 | # url from there...) | ||
2259 | 141 | @contextlib.contextmanager | ||
2260 | 142 | def trace(conn, program_name=None, filters=None): | ||
2261 | 143 | """ | ||
2262 | 144 | Context manager that records function calls in its context block. | ||
2263 | 145 | |||
2264 | 146 | e.g. given this code: | ||
2265 | 147 | |||
2266 | 148 | with sextant.trace("http://localhost:7474"): | ||
2267 | 149 | foo() | ||
2268 | 150 | bar() | ||
2269 | 151 | |||
2270 | 152 | The calls to foo() and bar() (and their callees, at any depth) will be | ||
2271 | 153 | recorded in the sextant database. | ||
2272 | 154 | |||
2273 | 155 | conn: | ||
2274 | 156 | Instance of SextantConnection that will be used to record calls. | ||
2275 | 157 | program_name: | ||
2276 | 158 | String used to refer to the traced program in sextant. Defaults to | ||
2277 | 159 | sys.argv[0]. | ||
2278 | 160 | filters: | ||
2279 | 161 | Optional iterable of entrails filters to apply. | ||
2280 | 162 | |||
2281 | 163 | """ | ||
2282 | 164 | if not _entrails_available: | ||
2283 | 165 | raise errors.MissingDependencyError( | ||
2284 | 166 | "Entrails is required to trace execution") | ||
2285 | 167 | |||
2286 | 168 | if program_name is None: | ||
2287 | 169 | program_name = sys.argv[0] | ||
2288 | 170 | |||
2289 | 171 | tracer = entrails.Entrails(filters=filters) | ||
2290 | 172 | tracer.add_output(_SextantOutput(conn, program_name)) | ||
2291 | 173 | |||
2292 | 174 | tracer.start_trace() | ||
2293 | 175 | try: | ||
2294 | 176 | yield | ||
2295 | 177 | finally: | ||
2296 | 178 | # Flush traced data. | ||
2297 | 179 | tracer.end_trace() | ||
2298 | 180 | |||
2299 | 0 | 181 | ||
2300 | === added file 'src/sextant/query.py' | |||
2301 | --- src/sextant/query.py 1970-01-01 00:00:00 +0000 | |||
2302 | +++ src/sextant/query.py 2014-08-15 12:04:08 +0000 | |||
2303 | @@ -0,0 +1,113 @@ | |||
2304 | 1 | # ----------------------------------------- | ||
2305 | 2 | # Sextant | ||
2306 | 3 | # Copyright 2014, Ensoft Ltd. | ||
2307 | 4 | # Author: James Harkin, Patrick Stevens | ||
2308 | 5 | # ----------------------------------------- | ||
2309 | 6 | #API for performing queries on the database | ||
2310 | 7 | |||
2311 | 8 | #!/usr/bin/python3 | ||
2312 | 9 | import argparse | ||
2313 | 10 | import requests, urllib # for different kinds of exception | ||
2314 | 11 | import logging | ||
2315 | 12 | |||
2316 | 13 | from . import db_api | ||
2317 | 14 | from .export import ProgramConverter | ||
2318 | 15 | |||
2319 | 16 | def query(remote_neo4j, input_query, program_name=None, argument_1=None, argument_2=None, suppress_common=False): | ||
2320 | 17 | |||
2321 | 18 | try: | ||
2322 | 19 | db = db_api.SextantConnection(remote_neo4j) | ||
2323 | 20 | except requests.exceptions.ConnectionError as err: | ||
2324 | 21 | logging.exception("Could not connect to Neo4J server {}. Are you sure it is running?".format(remote_neo4j)) | ||
2325 | 22 | logging.exception(str(err)) | ||
2326 | 23 | return 2 | ||
2327 | 24 | #Not supported in python 2 | ||
2328 | 25 | #except (urllib.exceptions.MaxRetryError): | ||
2329 | 26 | # logging.error("Connection was refused to {}. Are you sure the server is running?".format(remote_neo4j)) | ||
2330 | 27 | # return 2 | ||
2331 | 28 | except Exception as err: | ||
2332 | 29 | logging.exception(str(err)) | ||
2333 | 30 | return 2 | ||
2334 | 31 | |||
2335 | 32 | prog = None | ||
2336 | 33 | names_list = None | ||
2337 | 34 | |||
2338 | 35 | if input_query == 'functions-calling': | ||
2339 | 36 | if argument_1 == None: | ||
2340 | 37 | print('Supply one function name to functions-calling.') | ||
2341 | 38 | return 1 | ||
2342 | 39 | prog = db.get_all_functions_calling(program_name, argument_1) | ||
2343 | 40 | elif input_query == 'functions-called-by': | ||
2344 | 41 | if argument_1 == None: | ||
2345 | 42 | print('Supply one function name to functions-called-by.') | ||
2346 | 43 | return 1 | ||
2347 | 44 | prog = db.get_all_functions_called(program_name, argument_1) | ||
2348 | 45 | elif input_query == 'calls-between': | ||
2349 | 46 | if (argument_1 == None and argument_2 == None): | ||
2350 | 47 | print('Supply two function names to calls-between.') | ||
2351 | 48 | return 1 | ||
2352 | 49 | prog = db.get_call_paths(program_name, argument_1, argument_2) | ||
2353 | 50 | elif input_query == 'whole-graph': | ||
2354 | 51 | prog = db.get_whole_program(program_name) | ||
2355 | 52 | elif input_query == 'shortest-path': | ||
2356 | 53 | if argument_1 == None and argument_2 == None: | ||
2357 | 54 | print('Supply two function names to shortest-path.') | ||
2358 | 55 | return 1 | ||
2359 | 56 | prog = db.get_shortest_path_between_functions(program_name, argument_1, argument_2) | ||
2360 | 57 | elif input_query == 'return-all-function-names': | ||
2361 | 58 | if program_name != None: | ||
2362 | 59 | func_names = db.get_function_names(program_name) | ||
2363 | 60 | if func_names: | ||
2364 | 61 | names_list = list(func_names) | ||
2365 | 62 | else: | ||
2366 | 63 | print('No functions were found in program %s on server %s.' % (program_name, remote_neo4j)) | ||
2367 | 64 | else: | ||
2368 | 65 | list_of_programs = db.get_program_names() | ||
2369 | 66 | if not list_of_programs: | ||
2370 | 67 | print('Server %s database empty.' % (remote_neo4j)) | ||
2371 | 68 | return 0 | ||
2372 | 69 | func_list = [] | ||
2373 | 70 | for prog_name in list_of_programs: | ||
2374 | 71 | func_list += db.get_function_names(prog_name) | ||
2375 | 72 | if not func_list: | ||
2376 | 73 | print('Server %s contains no functions.' % (remote_neo4j)) | ||
2377 | 74 | else: | ||
2378 | 75 | names_list = func_list | ||
2379 | 76 | elif input_query == 'return-all-program-names': | ||
2380 | 77 | list_found = list(db.get_program_names()) | ||
2381 | 78 | if not list_found: | ||
2382 | 79 | print('No programs were found on server {}.'.format(remote_neo4j)) | ||
2383 | 80 | else: | ||
2384 | 81 | names_list = list_found | ||
2385 | 82 | else: | ||
2386 | 83 | print('Query unrecognised.') | ||
2387 | 84 | return 2 | ||
2388 | 85 | |||
2389 | 86 | if prog: | ||
2390 | 87 | print(ProgramConverter.to_yed_graphml(prog, suppress_common)) | ||
2391 | 88 | elif names_list is not None: | ||
2392 | 89 | print(names_list) | ||
2393 | 90 | else: | ||
2394 | 91 | print('Nothing was returned from the query.') | ||
2395 | 92 | |||
2396 | 93 | |||
2397 | 94 | def main(): | ||
2398 | 95 | argumentparser = argparse.ArgumentParser(description="Return GraphML representation or list from graph queries.") | ||
2399 | 96 | argumentparser.add_argument('--remote-neo4j', required=True, metavar="URL", help="URL of neo4j server", type=str, nargs=1) | ||
2400 | 97 | argumentparser.add_argument('--program-name', metavar="PROG_NAME", help="name of program as stored in the database", | ||
2401 | 98 | type=str, nargs=1) | ||
2402 | 99 | argumentparser.add_argument('--query', required=True, metavar="QUERY", | ||
2403 | 100 | help="""functions-calling, functions-called-by, calls-between, whole-graph, shortest-path, | ||
2404 | 101 | return-all-program-names or return-all-function-names; if return-all-function-names, | ||
2405 | 102 | supply argument -program-name""", type=str, nargs=1) | ||
2406 | 103 | argumentparser.add_argument('--funcs', metavar='FUNCS', help='functions to pass to the query', type=str, nargs='+') | ||
2407 | 104 | |||
2408 | 105 | parsed = argumentparser.parse_args() | ||
2409 | 106 | names_list = None | ||
2410 | 107 | |||
2411 | 108 | query(remote_neo4j=parsed.remote_neo4j[0], input_query=parsed.query[0], arguments=parsed.funcs, | ||
2412 | 109 | program_name=parsed.program_name[0]) | ||
2413 | 110 | |||
2414 | 111 | |||
2415 | 112 | if __name__ == '__main__': | ||
2416 | 113 | main() | ||
2417 | 0 | 114 | ||
2418 | === added file 'src/sextant/tests.py' | |||
2419 | --- src/sextant/tests.py 1970-01-01 00:00:00 +0000 | |||
2420 | +++ src/sextant/tests.py 2014-08-15 12:04:08 +0000 | |||
2421 | @@ -0,0 +1,261 @@ | |||
2422 | 1 | # ----------------------------------------- | ||
2423 | 2 | # Sextant | ||
2424 | 3 | # Copyright 2014, Ensoft Ltd. | ||
2425 | 4 | # Author: Patrick Stevens, James Harkin | ||
2426 | 5 | # ----------------------------------------- | ||
2427 | 6 | #Testing module | ||
2428 | 7 | |||
2429 | 8 | import unittest | ||
2430 | 9 | |||
2431 | 10 | from db_api import Function | ||
2432 | 11 | from db_api import FunctionQueryResult | ||
2433 | 12 | from db_api import SextantConnection | ||
2434 | 13 | from db_api import Validator | ||
2435 | 14 | |||
2436 | 15 | |||
2437 | 16 | class TestFunctionQueryResults(unittest.TestCase): | ||
2438 | 17 | def setUp(self): | ||
2439 | 18 | # we need to set up the remote database by using the neo4j_input_api | ||
2440 | 19 | self.remote_url = 'http://ensoft-sandbox:7474' | ||
2441 | 20 | |||
2442 | 21 | self.setter_connection = SextantConnection(self.remote_url) | ||
2443 | 22 | self.program_1_name = 'testprogram' | ||
2444 | 23 | self.upload_program = self.setter_connection.new_program(self.program_1_name) | ||
2445 | 24 | self.upload_program.add_function('func1') | ||
2446 | 25 | self.upload_program.add_function('func2') | ||
2447 | 26 | self.upload_program.add_function('func3') | ||
2448 | 27 | self.upload_program.add_function('func4') | ||
2449 | 28 | self.upload_program.add_function('func5') | ||
2450 | 29 | self.upload_program.add_function('func6') | ||
2451 | 30 | self.upload_program.add_function('func7') | ||
2452 | 31 | self.upload_program.add_function_call('func1', 'func2') | ||
2453 | 32 | self.upload_program.add_function_call('func1', 'func4') | ||
2454 | 33 | self.upload_program.add_function_call('func2', 'func1') | ||
2455 | 34 | self.upload_program.add_function_call('func2', 'func4') | ||
2456 | 35 | self.upload_program.add_function_call('func3', 'func5') | ||
2457 | 36 | self.upload_program.add_function_call('func4', 'func4') | ||
2458 | 37 | self.upload_program.add_function_call('func4', 'func5') | ||
2459 | 38 | self.upload_program.add_function_call('func5', 'func1') | ||
2460 | 39 | self.upload_program.add_function_call('func5', 'func2') | ||
2461 | 40 | self.upload_program.add_function_call('func5', 'func3') | ||
2462 | 41 | self.upload_program.add_function_call('func6', 'func7') | ||
2463 | 42 | |||
2464 | 43 | self.upload_program.commit() | ||
2465 | 44 | |||
2466 | 45 | self.one_node_program_name = 'testprogram1' | ||
2467 | 46 | self.upload_one_node_program = self.setter_connection.new_program(self.one_node_program_name) | ||
2468 | 47 | self.upload_one_node_program.add_function('lonefunc') | ||
2469 | 48 | |||
2470 | 49 | self.upload_one_node_program.commit() | ||
2471 | 50 | |||
2472 | 51 | self.empty_program_name = 'testprogramblank' | ||
2473 | 52 | self.upload_empty_program = self.setter_connection.new_program(self.empty_program_name) | ||
2474 | 53 | |||
2475 | 54 | self.upload_empty_program.commit() | ||
2476 | 55 | |||
2477 | 56 | self.getter_connection = SextantConnection(self.remote_url) | ||
2478 | 57 | |||
2479 | 58 | def tearDown(self): | ||
2480 | 59 | self.setter_connection.delete_program(self.upload_program.program_name) | ||
2481 | 60 | self.setter_connection.delete_program(self.upload_one_node_program.program_name) | ||
2482 | 61 | self.setter_connection.delete_program(self.upload_empty_program.program_name) | ||
2483 | 62 | del(self.setter_connection) | ||
2484 | 63 | |||
2485 | 64 | def test_17_get_call_paths(self): | ||
2486 | 65 | reference1 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) | ||
2487 | 66 | reference1.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'), | ||
2488 | 67 | Function(self.program_1_name, 'func3'), | ||
2489 | 68 | Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5')] | ||
2490 | 69 | reference1.functions[0].functions_i_call = reference1.functions[1:4:2] # func1 calls func2, func4 | ||
2491 | 70 | reference1.functions[1].functions_i_call = reference1.functions[0:4:3] # func2 calls func1, func4 | ||
2492 | 71 | reference1.functions[2].functions_i_call = [reference1.functions[4]] # func3 calls func5 | ||
2493 | 72 | reference1.functions[3].functions_i_call = reference1.functions[3:5] # func4 calls func4, func5 | ||
2494 | 73 | reference1.functions[4].functions_i_call = reference1.functions[0:3] # func5 calls func1, func2, func3 | ||
2495 | 74 | self.assertEquals(reference1, self.getter_connection.get_call_paths(self.program_1_name, 'func1', 'func2')) | ||
2496 | 75 | self.assertIsNone(self.getter_connection.get_call_paths('not a prog', 'func1', 'func2')) # shouldn't validation | ||
2497 | 76 | self.assertIsNone(self.getter_connection.get_call_paths('notaprogram', 'func1', 'func2')) | ||
2498 | 77 | self.assertIsNone(self.getter_connection.get_call_paths(self.program_1_name, 'notafunc', 'func2')) | ||
2499 | 78 | self.assertIsNone(self.getter_connection.get_call_paths(self.program_1_name, 'func1', 'notafunc')) | ||
2500 | 79 | |||
2501 | 80 | def test_02_get_whole_program(self): | ||
2502 | 81 | reference = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) | ||
2503 | 82 | reference.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'), | ||
2504 | 83 | Function(self.program_1_name, 'func3'), | ||
2505 | 84 | Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5'), | ||
2506 | 85 | Function(self.program_1_name, 'func6'), Function(self.program_1_name, 'func7')] | ||
2507 | 86 | reference.functions[0].functions_i_call = reference.functions[1:4:2] # func1 calls func2, func4 | ||
2508 | 87 | reference.functions[1].functions_i_call = reference.functions[0:4:3] # func2 calls func1, func4 | ||
2509 | 88 | reference.functions[2].functions_i_call = [reference.functions[4]] # func3 calls func5 | ||
2510 | 89 | reference.functions[3].functions_i_call = reference.functions[3:5] # func4 calls func4, func5 | ||
2511 | 90 | reference.functions[4].functions_i_call = reference.functions[0:3] # func5 calls func1, func2, func3 | ||
2512 | 91 | reference.functions[5].functions_i_call = [reference.functions[6]] # func6 calls func7 | ||
2513 | 92 | |||
2514 | 93 | |||
2515 | 94 | self.assertEqual(reference, self.getter_connection.get_whole_program(self.program_1_name)) | ||
2516 | 95 | self.assertIsNone(self.getter_connection.get_whole_program('nottherightprogramname')) | ||
2517 | 96 | |||
2518 | 97 | def test_03_get_whole_one_node_program(self): | ||
2519 | 98 | reference = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name) | ||
2520 | 99 | reference.functions = [Function(self.one_node_program_name, 'lonefunc')] | ||
2521 | 100 | |||
2522 | 101 | self.assertEqual(reference, self.getter_connection.get_whole_program(self.one_node_program_name)) | ||
2523 | 102 | |||
2524 | 103 | def test_04_get_whole_empty_program(self): | ||
2525 | 104 | reference = FunctionQueryResult(parent_db=None, program_name=self.empty_program_name) | ||
2526 | 105 | reference.functions = [] | ||
2527 | 106 | |||
2528 | 107 | self.assertEqual(reference, self.getter_connection.get_whole_program(self.empty_program_name)) | ||
2529 | 108 | |||
2530 | 109 | def test_05_get_function_names(self): | ||
2531 | 110 | reference = {'func1', 'func2', 'func3', 'func4', 'func5', 'func6', 'func7'} | ||
2532 | 111 | self.assertEqual(reference, self.getter_connection.get_function_names(self.program_1_name)) | ||
2533 | 112 | |||
2534 | 113 | def test_06_get_function_names_one_node_program(self): | ||
2535 | 114 | reference = {'lonefunc'} | ||
2536 | 115 | self.assertEqual(reference, self.getter_connection.get_function_names(self.one_node_program_name)) | ||
2537 | 116 | |||
2538 | 117 | def test_07_get_function_names_empty_program(self): | ||
2539 | 118 | reference = set() | ||
2540 | 119 | self.assertEqual(reference, self.getter_connection.get_function_names(self.empty_program_name)) | ||
2541 | 120 | |||
2542 | 121 | def test_09_validation_is_used(self): | ||
2543 | 122 | self.assertFalse(self.getter_connection.get_function_names('not alphanumeric')) | ||
2544 | 123 | self.assertFalse(self.getter_connection.get_whole_program('not alphanumeric')) | ||
2545 | 124 | self.assertFalse(self.getter_connection.check_program_exists('not alphanumeric')) | ||
2546 | 125 | self.assertFalse(self.getter_connection.check_function_exists('not alphanumeric', 'alpha')) | ||
2547 | 126 | self.assertFalse(self.getter_connection.check_function_exists('alpha', 'not alpha')) | ||
2548 | 127 | self.assertFalse(self.getter_connection.get_all_functions_called('alphaprogram', 'not alpha function')) | ||
2549 | 128 | self.assertFalse(self.getter_connection.get_all_functions_called('not alpha program', 'alphafunction')) | ||
2550 | 129 | self.assertFalse(self.getter_connection.get_all_functions_calling('not alpha program', 'alphafunction')) | ||
2551 | 130 | self.assertFalse(self.getter_connection.get_all_functions_calling('alphaprogram', 'not alpha function')) | ||
2552 | 131 | self.assertFalse(self.getter_connection.get_call_paths('not alpha program','alphafunc1', 'alphafunc2')) | ||
2553 | 132 | self.assertFalse(self.getter_connection.get_call_paths('alphaprogram','not alpha func 1', 'alphafunc2')) | ||
2554 | 133 | self.assertFalse(self.getter_connection.get_call_paths('alphaprogram','alphafunc1', 'not alpha func 2')) | ||
2555 | 134 | |||
2556 | 135 | def test_08_get_program_names(self): | ||
2557 | 136 | reference = {self.program_1_name, self.one_node_program_name, self.empty_program_name} | ||
2558 | 137 | self.assertEqual(reference, self.getter_connection.get_program_names()) | ||
2559 | 138 | |||
2560 | 139 | |||
2561 | 140 | def test_11_get_all_functions_called(self): | ||
2562 | 141 | reference1 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 1,2,3,4,5 component | ||
2563 | 142 | reference2 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 6,7 component | ||
2564 | 143 | reference3 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 7 component | ||
2565 | 144 | reference1.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'), | ||
2566 | 145 | Function(self.program_1_name, 'func3'), | ||
2567 | 146 | Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5')] | ||
2568 | 147 | reference2.functions = [Function(self.program_1_name, 'func6'), Function(self.program_1_name, 'func7')] | ||
2569 | 148 | reference3.functions = [] | ||
2570 | 149 | |||
2571 | 150 | reference1.functions[0].functions_i_call = reference1.functions[1:4:2] # func1 calls func2, func4 | ||
2572 | 151 | reference1.functions[1].functions_i_call = reference1.functions[0:4:3] # func2 calls func1, func4 | ||
2573 | 152 | reference1.functions[2].functions_i_call = [reference1.functions[4]] # func3 calls func5 | ||
2574 | 153 | reference1.functions[3].functions_i_call = reference1.functions[3:5] # func4 calls func4, func5 | ||
2575 | 154 | reference1.functions[4].functions_i_call = reference1.functions[0:3] # func5 calls func1, func2, func3 | ||
2576 | 155 | |||
2577 | 156 | reference2.functions[0].functions_i_call = [reference2.functions[1]] | ||
2578 | 157 | |||
2579 | 158 | self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func1')) | ||
2580 | 159 | self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func2')) | ||
2581 | 160 | self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func3')) | ||
2582 | 161 | self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func4')) | ||
2583 | 162 | self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func5')) | ||
2584 | 163 | |||
2585 | 164 | self.assertEquals(reference2, self.getter_connection.get_all_functions_called(self.program_1_name, 'func6')) | ||
2586 | 165 | |||
2587 | 166 | self.assertEquals(reference3, self.getter_connection.get_all_functions_called(self.program_1_name, 'func7')) | ||
2588 | 167 | |||
2589 | 168 | self.assertIsNone(self.getter_connection.get_all_functions_called(self.program_1_name, 'nottherightfunction')) | ||
2590 | 169 | self.assertIsNone(self.getter_connection.get_all_functions_called('nottherightprogram', 'func2')) | ||
2591 | 170 | |||
2592 | 171 | def test_12_get_all_functions_called_1(self): | ||
2593 | 172 | reference1 = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name) | ||
2594 | 173 | reference1.functions = [] | ||
2595 | 174 | |||
2596 | 175 | d=self.getter_connection.get_all_functions_called(self.one_node_program_name, 'lonefunc') | ||
2597 | 176 | self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.one_node_program_name, | ||
2598 | 177 | 'lonefunc')) | ||
2599 | 178 | self.assertIsNone(self.getter_connection.get_all_functions_called(self.one_node_program_name, | ||
2600 | 179 | 'not the right function')) | ||
2601 | 180 | self.assertIsNone(self.getter_connection.get_all_functions_called('not the right program', 'lonefunc')) | ||
2602 | 181 | |||
2603 | 182 | def test_13_get_all_functions_called_blank(self): | ||
2604 | 183 | reference1 = FunctionQueryResult(parent_db=None, program_name=self.empty_program_name) | ||
2605 | 184 | reference1.functions = [] | ||
2606 | 185 | |||
2607 | 186 | self.assertIsNone(self.getter_connection.get_all_functions_called(self.empty_program_name, | ||
2608 | 187 | 'not the right function')) | ||
2609 | 188 | |||
2610 | 189 | def test_14_get_all_functions_calling(self): | ||
2611 | 190 | reference1 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 1,2,3,4,5 component | ||
2612 | 191 | reference2 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 6,7 component | ||
2613 | 192 | reference3 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 7 component | ||
2614 | 193 | reference1.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'), | ||
2615 | 194 | Function(self.program_1_name, 'func3'), | ||
2616 | 195 | Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5')] | ||
2617 | 196 | |||
2618 | 197 | reference1.functions[0].functions_i_call = reference1.functions[1:4:2] # func1 calls func2, func4 | ||
2619 | 198 | reference1.functions[1].functions_i_call = reference1.functions[0:4:3] # func2 calls func1, func4 | ||
2620 | 199 | reference1.functions[2].functions_i_call = [reference1.functions[4]] # func3 calls func5 | ||
2621 | 200 | reference1.functions[3].functions_i_call = reference1.functions[3:5] # func4 calls func4, func5 | ||
2622 | 201 | reference1.functions[4].functions_i_call = reference1.functions[0:3] # func5 calls func1, func2, func3 | ||
2623 | 202 | |||
2624 | 203 | reference2.functions = [Function(self.program_1_name, 'func6'), Function(self.program_1_name, 'func7')] | ||
2625 | 204 | |||
2626 | 205 | reference2.functions[0].functions_i_call = [reference2.functions[1]] | ||
2627 | 206 | |||
2628 | 207 | reference3.functions = [Function(self.program_1_name, 'func6')] | ||
2629 | 208 | |||
2630 | 209 | reference3.functions = [] | ||
2631 | 210 | |||
2632 | 211 | self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func1')) | ||
2633 | 212 | self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func2')) | ||
2634 | 213 | self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func3')) | ||
2635 | 214 | self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func4')) | ||
2636 | 215 | self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func5')) | ||
2637 | 216 | |||
2638 | 217 | self.assertEquals(reference2, self.getter_connection.get_all_functions_calling(self.program_1_name,'func7')) | ||
2639 | 218 | |||
2640 | 219 | self.assertEquals(reference3, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func6')) | ||
2641 | 220 | |||
2642 | 221 | self.assertIsNone(self.getter_connection.get_all_functions_calling(self.program_1_name, 'nottherightfunction')) | ||
2643 | 222 | self.assertIsNone(self.getter_connection.get_all_functions_calling('nottherightprogram', 'func2')) | ||
2644 | 223 | |||
2645 | 224 | def test_15_get_all_functions_calling_one_node_prog(self): | ||
2646 | 225 | reference1 = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name) | ||
2647 | 226 | reference1.functions = [] | ||
2648 | 227 | self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.one_node_program_name, | ||
2649 | 228 | 'lonefunc')) | ||
2650 | 229 | self.assertIsNone(self.getter_connection.get_all_functions_calling(self.one_node_program_name, | ||
2651 | 230 | 'not the right function')) | ||
2652 | 231 | self.assertIsNone(self.getter_connection.get_all_functions_calling('not the right program', 'lonefunc')) | ||
2653 | 232 | |||
2654 | 233 | def test_16_get_all_functions_calling_blank_prog(self): | ||
2655 | 234 | reference1 = FunctionQueryResult(parent_db=None, program_name=self.empty_program_name) | ||
2656 | 235 | reference1.functions=[] | ||
2657 | 236 | |||
2658 | 237 | self.assertIsNone(self.getter_connection.get_all_functions_called(self.empty_program_name, | ||
2659 | 238 | 'not the right function')) | ||
2660 | 239 | |||
2661 | 240 | |||
2662 | 241 | |||
2663 | 242 | def test_18_get_call_paths_between_two_functions_one_node_prog(self): | ||
2664 | 243 | reference = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name) | ||
2665 | 244 | reference.functions = [] # that is, reference is the empty program with name self.one_node_program_name | ||
2666 | 245 | |||
2667 | 246 | self.assertEquals(self.getter_connection.get_call_paths(self.one_node_program_name, 'lonefunc', 'lonefunc'), | ||
2668 | 247 | reference) | ||
2669 | 248 | self.assertIsNone(self.getter_connection.get_call_paths(self.one_node_program_name, 'lonefunc', 'notafunc')) | ||
2670 | 249 | self.assertIsNone(self.getter_connection.get_call_paths(self.one_node_program_name, 'notafunc', 'notafunc')) | ||
2671 | 250 | |||
2672 | 251 | def test_10_validator(self): | ||
2673 | 252 | self.assertFalse(Validator.validate('')) | ||
2674 | 253 | self.assertTrue(Validator.validate('thisworks')) | ||
2675 | 254 | self.assertTrue(Validator.validate('th1sw0rks')) | ||
2676 | 255 | self.assertTrue(Validator.validate('12345')) | ||
2677 | 256 | self.assertFalse(Validator.validate('this does not work')) | ||
2678 | 257 | self.assertTrue(Validator.validate('this_does_work')) | ||
2679 | 258 | self.assertFalse(Validator.validate("'")) # string consisting of a single quote mark | ||
2680 | 259 | |||
2681 | 260 | if __name__ == '__main__': | ||
2682 | 261 | unittest.main() | ||
2683 | 0 | \ No newline at end of file | 262 | \ No newline at end of file |
2684 | 1 | 263 | ||
2685 | === added file 'src/sextant/update_db.py' | |||
2686 | --- src/sextant/update_db.py 1970-01-01 00:00:00 +0000 | |||
2687 | +++ src/sextant/update_db.py 2014-08-15 12:04:08 +0000 | |||
2688 | @@ -0,0 +1,78 @@ | |||
2689 | 1 | # ----------------------------------------- | ||
2690 | 2 | # Sextant | ||
2691 | 3 | # Copyright 2014, Ensoft Ltd. | ||
2692 | 4 | # Author: Patrick Stevens, using work from Patrick Stevens and James Harkin | ||
2693 | 5 | # ----------------------------------------- | ||
2694 | 6 | # Given a program file to upload, or a program name to delete from the server, does the right thing. | ||
2695 | 7 | |||
2696 | 8 | __all__ = ("upload_program", "delete_program") | ||
2697 | 9 | |||
2698 | 10 | from .db_api import SextantConnection, Validator | ||
2699 | 11 | from .objdump_parser import get_parsed_objects | ||
2700 | 12 | |||
2701 | 13 | import logging | ||
2702 | 14 | import requests | ||
2703 | 15 | |||
2704 | 16 | |||
2705 | 17 | def upload_program(file_path, db_url, alternative_name=None, not_object_file=False): | ||
2706 | 18 | """ | ||
2707 | 19 | Uploads a program to the remote database. | ||
2708 | 20 | :param file_path: the path to the local file we wish to upload | ||
2709 | 21 | :param db_url: the URL of the database (eg. http://localhost:7474) | ||
2710 | 22 | :param alternative_name: a name to give the program to override the default | ||
2711 | 23 | :param object_file: bool(the file is an objdump text output file, rather than a compiled binary) | ||
2712 | 24 | :return: 1 if the program already exists in database, 2 if there was a connection error | ||
2713 | 25 | """ | ||
2714 | 26 | try: | ||
2715 | 27 | connection = SextantConnection(db_url) | ||
2716 | 28 | except requests.exceptions.ConnectionError as err: | ||
2717 | 29 | logging.exception("Could not connect to Neo4J server {}. Are you sure it is running?".format(db_url)) | ||
2718 | 30 | return 2 | ||
2719 | 31 | #except urllib.exceptions.MaxRetryError: | ||
2720 | 32 | # logging.error("Connection was refused to {}. Are you sure the server is running?".format(db_url)) | ||
2721 | 33 | # return 2 | ||
2722 | 34 | |||
2723 | 35 | program_names = connection.get_program_names() | ||
2724 | 36 | if alternative_name is None: | ||
2725 | 37 | if Validator.sanitise(file_path) in program_names: | ||
2726 | 38 | logging.error("There is already a program under this name; please delete the previous one with the same name " | ||
2727 | 39 | "and retry, or rename the input file.") | ||
2728 | 40 | return 1 | ||
2729 | 41 | else: | ||
2730 | 42 | if Validator.sanitise(alternative_name) in program_names: | ||
2731 | 43 | logging.error("There is already a program under this name; please delete the previous one with the same name " | ||
2732 | 44 | "and retry, or rename the input file.") | ||
2733 | 45 | return 1 | ||
2734 | 46 | |||
2735 | 47 | parsed_objects = get_parsed_objects(filepath=file_path, sections_to_view=['.text'], | ||
2736 | 48 | not_object_file=not_object_file, ignore_function_pointers=False) | ||
2737 | 49 | |||
2738 | 50 | logging.warning('Objdump has parsed!') | ||
2739 | 51 | |||
2740 | 52 | if alternative_name is None: | ||
2741 | 53 | program_representation = connection.new_program(Validator.sanitise(file_path)) | ||
2742 | 54 | else: | ||
2743 | 55 | program_representation = connection.new_program(Validator.sanitise(alternative_name)) | ||
2744 | 56 | |||
2745 | 57 | |||
2746 | 58 | |||
2747 | 59 | for obj in parsed_objects: | ||
2748 | 60 | for called in obj.what_do_i_call: | ||
2749 | 61 | if not program_representation.add_function_call(obj.name, called[-1]): # called is a tuple (address, name) | ||
2750 | 62 | logging.error('Validation error: {} calling {}'.format(obj.name, called[-1])) | ||
2751 | 63 | |||
2752 | 64 | logging.warning('Sending {} named objects to server {}...'.format(len(parsed_objects), db_url)) | ||
2753 | 65 | program_representation.commit() | ||
2754 | 66 | logging.info('Sending complete! Exiting.') | ||
2755 | 67 | |||
2756 | 68 | |||
2757 | 69 | def delete_program(program_name, db_url): | ||
2758 | 70 | """ | ||
2759 | 71 | Deletes a program with the specified name from the database. | ||
2760 | 72 | :param program_name: the name of the program to delete | ||
2761 | 73 | :param db_url: the URL of the database (eg. http://localhost:7474) | ||
2762 | 74 | :return: bool(success) | ||
2763 | 75 | """ | ||
2764 | 76 | connection = SextantConnection(db_url) | ||
2765 | 77 | connection.delete_program(program_name) | ||
2766 | 78 | print('Deleted {} successfully.'.format(program_name)) | ||
2767 | 0 | 79 | ||
2768 | === added directory 'src/sextant/web' | |||
2769 | === added file 'src/sextant/web/__init__.py' | |||
2770 | --- src/sextant/web/__init__.py 1970-01-01 00:00:00 +0000 | |||
2771 | +++ src/sextant/web/__init__.py 2014-08-15 12:04:08 +0000 | |||
2772 | @@ -0,0 +1,8 @@ | |||
2773 | 1 | #---------------------------------------------------------------------------- | ||
2774 | 2 | # __init__.py -- Web package root | ||
2775 | 3 | # | ||
2776 | 4 | # July 2014, Phil Connell | ||
2777 | 5 | # | ||
2778 | 6 | # (c) Ensoft Ltd, 2014 | ||
2779 | 7 | #---------------------------------------------------------------------------- | ||
2780 | 8 | |||
2781 | 0 | 9 | ||
2782 | === added file 'src/sextant/web/server.py' | |||
2783 | --- src/sextant/web/server.py 1970-01-01 00:00:00 +0000 | |||
2784 | +++ src/sextant/web/server.py 2014-08-15 12:04:08 +0000 | |||
2785 | @@ -0,0 +1,323 @@ | |||
2786 | 1 | #!/usr/bin/python2 | ||
2787 | 2 | # ----------------------------------------- | ||
2788 | 3 | # Sextant | ||
2789 | 4 | # Copyright 2014, Ensoft Ltd. | ||
2790 | 5 | # Author: Patrick Stevens, James Harkin | ||
2791 | 6 | # ----------------------------------------- | ||
2792 | 7 | # Note: this must be run in Python 2. | ||
2793 | 8 | |||
2794 | 9 | from twisted.web.server import Site, NOT_DONE_YET | ||
2795 | 10 | from twisted.web.resource import Resource | ||
2796 | 11 | from twisted.web.static import File | ||
2797 | 12 | from twisted.internet import reactor | ||
2798 | 13 | from twisted.internet.threads import deferToThread | ||
2799 | 14 | from twisted.internet import defer | ||
2800 | 15 | |||
2801 | 16 | import logging | ||
2802 | 17 | |||
2803 | 18 | import os | ||
2804 | 19 | import sys # hack to get the Sextant module imported | ||
2805 | 20 | sys.path.append(os.path.realpath('../..')) | ||
2806 | 21 | |||
2807 | 22 | import json | ||
2808 | 23 | import requests | ||
2809 | 24 | |||
2810 | 25 | import sextant.db_api as db_api | ||
2811 | 26 | import sextant.export as export | ||
2812 | 27 | import tempfile | ||
2813 | 28 | import subprocess | ||
2814 | 29 | |||
2815 | 30 | from cgi import escape # deprecated in Python 3 in favour of html.escape, but we're stuck on Python 2 | ||
2816 | 31 | |||
2817 | 32 | database_url = None # the URL to access the database instance | ||
2818 | 33 | |||
2819 | 34 | class Echoer(Resource): | ||
2820 | 35 | # designed to take one name argument | ||
2821 | 36 | |||
2822 | 37 | def render_GET(self, request): | ||
2823 | 38 | if "name" not in request.args: | ||
2824 | 39 | return '<html><body>Greetings, unnamed stranger.</body></html>' | ||
2825 | 40 | |||
2826 | 41 | arg = escape(request.args["name"][0]) | ||
2827 | 42 | return '<html><body>Hello %s!</body></html>' % arg | ||
2828 | 43 | |||
2829 | 44 | |||
2830 | 45 | class SVGRenderer(Resource): | ||
2831 | 46 | |||
2832 | 47 | def error_creating_neo4j_connection(self, failure): | ||
2833 | 48 | self.write("Error creating Neo4J connection: %s\n") % failure.getErrorMessage() | ||
2834 | 49 | |||
2835 | 50 | @staticmethod | ||
2836 | 51 | def create_neo4j_connection(): | ||
2837 | 52 | return db_api.SextantConnection(database_url) | ||
2838 | 53 | |||
2839 | 54 | @staticmethod | ||
2840 | 55 | def check_program_exists(connection, name): | ||
2841 | 56 | return connection.check_program_exists(name) | ||
2842 | 57 | |||
2843 | 58 | @staticmethod | ||
2844 | 59 | def get_whole_program(connection, name): | ||
2845 | 60 | return connection.get_whole_program(name) | ||
2846 | 61 | |||
2847 | 62 | @staticmethod | ||
2848 | 63 | def get_functions_calling(connection, progname, funcname): | ||
2849 | 64 | return connection.get_all_functions_calling(progname, funcname) | ||
2850 | 65 | |||
2851 | 66 | @staticmethod | ||
2852 | 67 | def get_plot(program, suppress_common_functions=False): | ||
2853 | 68 | graph_dot = export.ProgramConverter.to_dot(program, suppress_common_functions) | ||
2854 | 69 | |||
2855 | 70 | file_written_to = tempfile.NamedTemporaryFile(delete=False) | ||
2856 | 71 | file_out = tempfile.NamedTemporaryFile(delete=False) | ||
2857 | 72 | file_written_to.write(graph_dot) | ||
2858 | 73 | file_written_to.close() | ||
2859 | 74 | subprocess.call(['dot', '-Tsvg', '-Kdot', '-o', file_out.name, file_written_to.name]) | ||
2860 | 75 | |||
2861 | 76 | output = file_out.read().encode() | ||
2862 | 77 | file_out.close() | ||
2863 | 78 | return output | ||
2864 | 79 | |||
2865 | 80 | @defer.inlineCallbacks | ||
2866 | 81 | def _render_plot(self, request): | ||
2867 | 82 | if "program_name" not in request.args: | ||
2868 | 83 | request.setResponseCode(400) | ||
2869 | 84 | request.write("Supply 'program_name' parameter.") | ||
2870 | 85 | request.finish() | ||
2871 | 86 | defer.returnValue(None) | ||
2872 | 87 | |||
2873 | 88 | logging.info('enter') | ||
2874 | 89 | name = request.args["program_name"][0] | ||
2875 | 90 | |||
2876 | 91 | try: | ||
2877 | 92 | suppress_common = request.args["suppress_common"][0] | ||
2878 | 93 | except KeyError: | ||
2879 | 94 | suppress_common = False | ||
2880 | 95 | |||
2881 | 96 | if suppress_common == 'null' or suppress_common == 'true': | ||
2882 | 97 | suppress_common = True | ||
2883 | 98 | else: | ||
2884 | 99 | suppress_common = False | ||
2885 | 100 | |||
2886 | 101 | try: | ||
2887 | 102 | neo4jconnection = yield deferToThread(self.create_neo4j_connection) | ||
2888 | 103 | except requests.exceptions.ConnectionError: | ||
2889 | 104 | request.setResponseCode(502) # Bad Gateway | ||
2890 | 105 | request.write("Could not reach Neo4j server at {}".format(database_url)) | ||
2891 | 106 | request.finish() | ||
2892 | 107 | defer.returnValue(None) | ||
2893 | 108 | neo4jconnection = None # to silence the "referenced before assignment" warnings later | ||
2894 | 109 | |||
2895 | 110 | logging.info('created') | ||
2896 | 111 | exists = yield deferToThread(self.check_program_exists, neo4jconnection, name) | ||
2897 | 112 | if not exists: | ||
2898 | 113 | request.setResponseCode(404) | ||
2899 | 114 | logging.info('returning nonexistent') | ||
2900 | 115 | request.write("Name %s not found." % (escape(name))) | ||
2901 | 116 | request.finish() | ||
2902 | 117 | defer.returnValue(None) | ||
2903 | 118 | |||
2904 | 119 | logging.info('done created') | ||
2905 | 120 | allowed_queries = ("whole_program", "functions_calling", "functions_called_by", "call_paths", "shortest_path") | ||
2906 | 121 | |||
2907 | 122 | if "query" not in request.args: | ||
2908 | 123 | query = "whole_program" | ||
2909 | 124 | else: | ||
2910 | 125 | query = request.args["query"][0] | ||
2911 | 126 | |||
2912 | 127 | if query not in allowed_queries: | ||
2913 | 128 | # raise 400 Bad Request error | ||
2914 | 129 | request.setResponseCode(400) | ||
2915 | 130 | request.write("Supply 'query' parameter, default is whole_program, allowed %s." % str(allowed_queries)) | ||
2916 | 131 | request.finish() | ||
2917 | 132 | defer.returnValue(None) | ||
2918 | 133 | |||
2919 | 134 | if query == 'whole_program': | ||
2920 | 135 | program = yield deferToThread(self.get_whole_program, neo4jconnection, name) | ||
2921 | 136 | elif query == 'functions_calling': | ||
2922 | 137 | if 'func1' not in request.args: | ||
2923 | 138 | # raise 400 Bad Request error | ||
2924 | 139 | request.setResponseCode(400) | ||
2925 | 140 | request.write("Supply 'func1' parameter to functions_calling.") | ||
2926 | 141 | request.finish() | ||
2927 | 142 | defer.returnValue(None) | ||
2928 | 143 | func1 = request.args['func1'][0] | ||
2929 | 144 | program = yield deferToThread(self.get_functions_calling, neo4jconnection, name, func1) | ||
2930 | 145 | elif query == 'functions_called_by': | ||
2931 | 146 | if 'func1' not in request.args: | ||
2932 | 147 | # raise 400 Bad Request error | ||
2933 | 148 | request.setResponseCode(400) | ||
2934 | 149 | request.write("Supply 'func1' parameter to functions_called_by.") | ||
2935 | 150 | request.finish() | ||
2936 | 151 | defer.returnValue(None) | ||
2937 | 152 | func1 = request.args['func1'][0] | ||
2938 | 153 | program = yield deferToThread(neo4jconnection.get_all_functions_called, name, func1) | ||
2939 | 154 | elif query == 'call_paths': | ||
2940 | 155 | if 'func1' not in request.args: | ||
2941 | 156 | # raise 400 Bad Request error | ||
2942 | 157 | request.setResponseCode(400) | ||
2943 | 158 | request.write("Supply 'func1' parameter to call_paths.") | ||
2944 | 159 | request.finish() | ||
2945 | 160 | defer.returnValue(None) | ||
2946 | 161 | if 'func2' not in request.args: | ||
2947 | 162 | # raise 400 Bad Request error | ||
2948 | 163 | request.setResponseCode(400) | ||
2949 | 164 | request.write("Supply 'func2' parameter to call_paths.") | ||
2950 | 165 | request.finish() | ||
2951 | 166 | defer.returnValue(None) | ||
2952 | 167 | |||
2953 | 168 | func1 = request.args['func1'][0] | ||
2954 | 169 | func2 = request.args['func2'][0] | ||
2955 | 170 | program = yield deferToThread(neo4jconnection.get_call_paths, name, func1, func2) | ||
2956 | 171 | elif query == 'shortest_path': | ||
2957 | 172 | if 'func1' not in request.args: | ||
2958 | 173 | # raise 400 Bad Request error | ||
2959 | 174 | request.setResponseCode(400) | ||
2960 | 175 | request.write("Supply 'func1' parameter to shortest_path.") | ||
2961 | 176 | request.finish() | ||
2962 | 177 | defer.returnValue(None) | ||
2963 | 178 | if 'func2' not in request.args: | ||
2964 | 179 | # raise 400 Bad Request error | ||
2965 | 180 | request.setResponseCode(400) | ||
2966 | 181 | request.write("Supply 'func2' parameter to shortest_path.") | ||
2967 | 182 | request.finish() | ||
2968 | 183 | defer.returnValue(None) | ||
2969 | 184 | |||
2970 | 185 | func1 = request.args['func1'][0] | ||
2971 | 186 | func2 = request.args['func2'][0] | ||
2972 | 187 | program = yield deferToThread(neo4jconnection.get_shortest_path_between_functions, name, func1, func2) | ||
2973 | 188 | |||
2974 | 189 | else: # unrecognised query, so we need to raise a Bad Request response | ||
2975 | 190 | request.setResponseCode(400) | ||
2976 | 191 | request.write("Query %s not recognised." % escape(query)) | ||
2977 | 192 | request.finish() | ||
2978 | 193 | defer.returnValue(None) | ||
2979 | 194 | program = None # silences the "referenced before assignment" warnings | ||
2980 | 195 | |||
2981 | 196 | if program is None: | ||
2982 | 197 | request.setResponseCode(404) | ||
2983 | 198 | request.write("At least one of the input functions was not found in program %s." % (escape(name))) | ||
2984 | 199 | request.finish() | ||
2985 | 200 | defer.returnValue(None) | ||
2986 | 201 | |||
2987 | 202 | logging.info('getting plot') | ||
2988 | 203 | logging.info(program) | ||
2989 | 204 | if not program.functions: # we got an empty program back: the program is in the Sextant but has no functions | ||
2990 | 205 | request.setResponseCode(204) | ||
2991 | 206 | request.finish() | ||
2992 | 207 | defer.returnValue(None) | ||
2993 | 208 | |||
2994 | 209 | output = yield deferToThread(self.get_plot, program, suppress_common) | ||
2995 | 210 | request.setHeader("content-type", "image/svg+xml") | ||
2996 | 211 | |||
2997 | 212 | logging.info('SVG: return') | ||
2998 | 213 | request.write(output) | ||
2999 | 214 | request.finish() | ||
3000 | 215 | |||
3001 | 216 | def render_GET(self, request): | ||
3002 | 217 | self._render_plot(request) | ||
3003 | 218 | return NOT_DONE_YET | ||
3004 | 219 | |||
3005 | 220 | |||
3006 | 221 | class GraphProperties(Resource): | ||
3007 | 222 | |||
3008 | 223 | @staticmethod | ||
3009 | 224 | def _get_connection(): | ||
3010 | 225 | return db_api.SextantConnection(database_url) | ||
3011 | 226 | |||
3012 | 227 | @staticmethod | ||
3013 | 228 | def _get_program_names(connection): | ||
3014 | 229 | return connection.get_program_names() | ||
3015 | 230 | |||
3016 | 231 | @staticmethod | ||
3017 | 232 | def _get_function_names(connection, program_name): | ||
3018 | 233 | return connection.get_function_names(program_name) | ||
3019 | 234 | |||
3020 | 235 | @defer.inlineCallbacks | ||
3021 | 236 | def _render_GET(self, request): | ||
3022 | 237 | if "query" not in request.args: | ||
3023 | 238 | request.setResponseCode(400) | ||
3024 | 239 | request.setHeader("content-type", "text/plain") | ||
3025 | 240 | request.write("Supply 'query' parameter of 'programs' or 'functions'.") | ||
3026 | 241 | request.finish() | ||
3027 | 242 | defer.returnValue(None) | ||
3028 | 243 | |||
3029 | 244 | query = request.args['query'][0] | ||
3030 | 245 | |||
3031 | 246 | logging.info('Properties: about to get_connection') | ||
3032 | 247 | |||
3033 | 248 | try: | ||
3034 | 249 | neo4j_connection = yield deferToThread(self._get_connection) | ||
3035 | 250 | except Exception: | ||
3036 | 251 | request.setResponseCode(502) # Bad Gateway | ||
3037 | 252 | request.write("Could not reach Neo4j server at {}.".format(database_url)) | ||
3038 | 253 | request.finish() | ||
3039 | 254 | defer.returnValue(None) | ||
3040 | 255 | neo4j_connection = None # just to silence the "referenced before assignment" warnings | ||
3041 | 256 | |||
3042 | 257 | logging.info('got connection') | ||
3043 | 258 | |||
3044 | 259 | if query == 'programs': | ||
3045 | 260 | request.setHeader("content-type", "application/json") | ||
3046 | 261 | prognames = yield deferToThread(self._get_program_names, neo4j_connection) | ||
3047 | 262 | request.write(json.dumps(list(prognames))) | ||
3048 | 263 | request.finish() | ||
3049 | 264 | defer.returnValue(None) | ||
3050 | 265 | |||
3051 | 266 | elif query == 'functions': | ||
3052 | 267 | if "program_name" not in request.args: | ||
3053 | 268 | request.setResponseCode(400) | ||
3054 | 269 | request.setHeader("content-type", "text/plain") | ||
3055 | 270 | request.write("Supply 'program_name' parameter to ?query=functions.") | ||
3056 | 271 | request.finish() | ||
3057 | 272 | defer.returnValue(None) | ||
3058 | 273 | program_name = request.args['program_name'][0] | ||
3059 | 274 | |||
3060 | 275 | funcnames = yield deferToThread(self._get_function_names, neo4j_connection, program_name) | ||
3061 | 276 | if funcnames is None: | ||
3062 | 277 | request.setResponseCode(404) | ||
3063 | 278 | request.setHeader("content-type", "text/plain") | ||
3064 | 279 | request.write("No program with name %s was found in the Sextant." % escape(program_name)) | ||
3065 | 280 | request.finish() | ||
3066 | 281 | defer.returnValue(None) | ||
3067 | 282 | |||
3068 | 283 | request.setHeader("content-type", "application/json") | ||
3069 | 284 | request.write(json.dumps(list(funcnames))) | ||
3070 | 285 | request.finish() | ||
3071 | 286 | defer.returnValue(None) | ||
3072 | 287 | |||
3073 | 288 | else: | ||
3074 | 289 | request.setResponseCode(400) | ||
3075 | 290 | request.setHeader("content-type", "text/plain") | ||
3076 | 291 | request.write("'Query' parameter should be 'programs' or 'functions'.") | ||
3077 | 292 | request.finish() | ||
3078 | 293 | defer.returnValue(None) | ||
3079 | 294 | |||
3080 | 295 | def render_GET(self, request): | ||
3081 | 296 | self._render_GET(request) | ||
3082 | 297 | return NOT_DONE_YET | ||
3083 | 298 | |||
3084 | 299 | |||
3085 | 300 | def serve_site(input_database_url='http://localhost:7474', port=2905): | ||
3086 | 301 | |||
3087 | 302 | global database_url | ||
3088 | 303 | database_url = input_database_url | ||
3089 | 304 | # serve static directory at root | ||
3090 | 305 | root = File(os.path.join( | ||
3091 | 306 | os.path.dirname(os.path.abspath(__file__)), | ||
3092 | 307 | "..", "..", "..", "resources", "web")) | ||
3093 | 308 | |||
3094 | 309 | # serve a dynamic Echoer webpage at /echoer.html | ||
3095 | 310 | root.putChild("echoer.html", Echoer()) | ||
3096 | 311 | |||
3097 | 312 | # serve a dynamic webpage at /Sextant_properties to return graph properties | ||
3098 | 313 | root.putChild("database_properties", GraphProperties()) | ||
3099 | 314 | |||
3100 | 315 | # serve a generated SVG at /output_graph.svg | ||
3101 | 316 | root.putChild('output_graph.svg', SVGRenderer()) | ||
3102 | 317 | |||
3103 | 318 | factory = Site(root) | ||
3104 | 319 | reactor.listenTCP(port, factory) | ||
3105 | 320 | reactor.run() | ||
3106 | 321 | |||
3107 | 322 | if __name__ == '__main__': | ||
3108 | 323 | serve_site() |
Merging in without a review, to get trunk in sync with latest code.
Full review to be done at some point.