1'''
2Entry-point module to start the code-completion server for PyDev.
3
4@author Fabio Zadrozny
5'''
6IS_PYTHON3K = 0
7try:
8    import __builtin__
9except ImportError:
10    import builtins as __builtin__  # Python 3.0
11    IS_PYTHON3K = 1
12
13from _pydevd_bundle.pydevd_constants import IS_JYTHON
14
15if IS_JYTHON:
16    import java.lang  # @UnresolvedImport
17    SERVER_NAME = 'jycompletionserver'
18    from _pydev_bundle import _pydev_jy_imports_tipper
19    _pydev_imports_tipper = _pydev_jy_imports_tipper
20
21else:
22    # it is python
23    SERVER_NAME = 'pycompletionserver'
24    from _pydev_bundle import _pydev_imports_tipper
25
26
27from _pydev_imps._pydev_saved_modules import socket
28
29import sys
30if sys.platform == "darwin":
31    # See: https://sourceforge.net/projects/pydev/forums/forum/293649/topic/3454227
32    try:
33        import _CF  # Don't fail if it doesn't work -- do it because it must be loaded on the main thread! @UnresolvedImport @UnusedImport
34    except:
35        pass
36
37
38# initial sys.path
39_sys_path = []
40for p in sys.path:
41    # changed to be compatible with 1.5
42    _sys_path.append(p)
43
44# initial sys.modules
45_sys_modules = {}
46for name, mod in sys.modules.items():
47    _sys_modules[name] = mod
48
49
50import traceback
51
52from _pydev_imps._pydev_saved_modules import time
53
54try:
55    import StringIO
56except:
57    import io as StringIO #Python 3.0
58
59try:
60    from urllib import quote_plus, unquote_plus
61except ImportError:
62    from urllib.parse import quote_plus, unquote_plus #Python 3.0
63
64INFO1 = 1
65INFO2 = 2
66WARN = 4
67ERROR = 8
68
69DEBUG = INFO1 | ERROR
70
71def dbg(s, prior):
72    if prior & DEBUG != 0:
73        sys.stdout.write('%s\n' % (s,))
74#        f = open('c:/temp/test.txt', 'a')
75#        print_ >> f, s
76#        f.close()
77
78from _pydev_bundle import pydev_localhost
79HOST = pydev_localhost.get_localhost() # Symbolic name meaning the local host
80
81MSG_KILL_SERVER = '@@KILL_SERVER_END@@'
82MSG_COMPLETIONS = '@@COMPLETIONS'
83MSG_END = 'END@@'
84MSG_INVALID_REQUEST = '@@INVALID_REQUEST'
85MSG_JYTHON_INVALID_REQUEST = '@@JYTHON_INVALID_REQUEST'
86MSG_CHANGE_DIR = '@@CHANGE_DIR:'
87MSG_OK = '@@MSG_OK_END@@'
88MSG_IMPORTS = '@@IMPORTS:'
89MSG_PYTHONPATH = '@@PYTHONPATH_END@@'
90MSG_CHANGE_PYTHONPATH = '@@CHANGE_PYTHONPATH:'
91MSG_JEDI = '@@MSG_JEDI:'
92MSG_SEARCH = '@@SEARCH'
93
94BUFFER_SIZE = 1024
95
96
97
98currDirModule = None
99
100def complete_from_dir(directory):
101    '''
102    This is necessary so that we get the imports from the same directory where the file
103    we are completing is located.
104    '''
105    global currDirModule
106    if currDirModule is not None:
107        if len(sys.path) > 0 and sys.path[0] == currDirModule:
108            del sys.path[0]
109
110    currDirModule = directory
111    sys.path.insert(0, directory)
112
113
114def change_python_path(pythonpath):
115    '''Changes the pythonpath (clears all the previous pythonpath)
116
117    @param pythonpath: string with paths separated by |
118    '''
119
120    split = pythonpath.split('|')
121    sys.path = []
122    for path in split:
123        path = path.strip()
124        if len(path) > 0:
125            sys.path.append(path)
126
127
128class Processor:
129
130    def __init__(self):
131        # nothing to do
132        return
133
134    def remove_invalid_chars(self, msg):
135        try:
136            msg = str(msg)
137        except UnicodeDecodeError:
138            pass
139
140        if msg:
141            try:
142                return quote_plus(msg)
143            except:
144                sys.stdout.write('error making quote plus in %s\n' % (msg,))
145                raise
146        return ' '
147
148    def format_completion_message(self, defFile, completionsList):
149        '''
150        Format the completions suggestions in the following format:
151        @@COMPLETIONS(modFile(token,description),(token,description),(token,description))END@@
152        '''
153        compMsg = []
154        compMsg.append('%s' % defFile)
155        for tup in completionsList:
156            compMsg.append(',')
157
158            compMsg.append('(')
159            compMsg.append(str(self.remove_invalid_chars(tup[0])))  # token
160            compMsg.append(',')
161            compMsg.append(self.remove_invalid_chars(tup[1]))  # description
162
163            if(len(tup) > 2):
164                compMsg.append(',')
165                compMsg.append(self.remove_invalid_chars(tup[2]))  # args - only if function.
166
167            if(len(tup) > 3):
168                compMsg.append(',')
169                compMsg.append(self.remove_invalid_chars(tup[3]))  # TYPE
170
171            compMsg.append(')')
172
173        return '%s(%s)%s' % (MSG_COMPLETIONS, ''.join(compMsg), MSG_END)
174
175class Exit(Exception):
176    pass
177
178class CompletionServer:
179
180    def __init__(self, port):
181        self.ended = False
182        self.port = port
183        self.socket = None  # socket to send messages.
184        self.exit_process_on_kill = True
185        self.processor = Processor()
186
187
188    def connect_to_server(self):
189        from _pydev_imps._pydev_saved_modules import socket
190
191        self.socket = s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
192        try:
193            s.connect((HOST, self.port))
194        except:
195            sys.stderr.write('Error on connect_to_server with parameters: host: %s port: %s\n' % (HOST, self.port))
196            raise
197
198    def get_completions_message(self, defFile, completionsList):
199        '''
200        get message with completions.
201        '''
202        return self.processor.format_completion_message(defFile, completionsList)
203
204    def get_token_and_data(self, data):
205        '''
206        When we receive this, we have 'token):data'
207        '''
208        token = ''
209        for c in data:
210            if c != ')':
211                token = token + c
212            else:
213                break;
214
215        return token, data.lstrip(token + '):')
216
217    def emulated_sendall(self, msg):
218        MSGLEN = 1024 * 20
219
220        totalsent = 0
221        while totalsent < MSGLEN:
222            sent = self.socket.send(msg[totalsent:])
223            if sent == 0:
224                return
225            totalsent = totalsent + sent
226
227
228    def send(self, msg):
229        if not hasattr(self.socket, 'sendall'):
230            #Older versions (jython 2.1)
231            self.emulated_sendall(msg)
232        else:
233            if IS_PYTHON3K:
234                self.socket.sendall(bytearray(msg, 'utf-8'))
235            else:
236                self.socket.sendall(msg)
237
238
239    def run(self):
240        # Echo server program
241        try:
242            from _pydev_bundle import _pydev_log
243            log = _pydev_log.Log()
244
245            dbg(SERVER_NAME + ' connecting to java server on %s (%s)' % (HOST, self.port) , INFO1)
246            # after being connected, create a socket as a client.
247            self.connect_to_server()
248
249            dbg(SERVER_NAME + ' Connected to java server', INFO1)
250
251
252            while not self.ended:
253                data = ''
254
255                while data.find(MSG_END) == -1:
256                    received = self.socket.recv(BUFFER_SIZE)
257                    if len(received) == 0:
258                        raise Exit()  # ok, connection ended
259                    if IS_PYTHON3K:
260                        data = data + received.decode('utf-8')
261                    else:
262                        data = data + received
263
264                try:
265                    try:
266                        if data.find(MSG_KILL_SERVER) != -1:
267                            dbg(SERVER_NAME + ' kill message received', INFO1)
268                            # break if we received kill message.
269                            self.ended = True
270                            raise Exit()
271
272                        dbg(SERVER_NAME + ' starting keep alive thread', INFO2)
273
274                        if data.find(MSG_PYTHONPATH) != -1:
275                            comps = []
276                            for p in _sys_path:
277                                comps.append((p, ' '))
278                            self.send(self.get_completions_message(None, comps))
279
280                        else:
281                            data = data[:data.rfind(MSG_END)]
282
283                            if data.startswith(MSG_IMPORTS):
284                                data = data[len(MSG_IMPORTS):]
285                                data = unquote_plus(data)
286                                defFile, comps = _pydev_imports_tipper.generate_tip(data, log)
287                                self.send(self.get_completions_message(defFile, comps))
288
289                            elif data.startswith(MSG_CHANGE_PYTHONPATH):
290                                data = data[len(MSG_CHANGE_PYTHONPATH):]
291                                data = unquote_plus(data)
292                                change_python_path(data)
293                                self.send(MSG_OK)
294
295                            elif data.startswith(MSG_JEDI):
296                                data = data[len(MSG_JEDI):]
297                                data = unquote_plus(data)
298                                line, column, encoding, path, source = data.split('|', 4)
299                                try:
300                                    import jedi  # @UnresolvedImport
301                                except:
302                                    self.send(self.get_completions_message(None, [('Error on import jedi', 'Error importing jedi', '')]))
303                                else:
304                                    script = jedi.Script(
305                                        # Line +1 because it expects lines 1-based (and col 0-based)
306                                        source=source,
307                                        line=int(line) + 1,
308                                        column=int(column),
309                                        source_encoding=encoding,
310                                        path=path,
311                                    )
312                                    lst = []
313                                    for completion in script.completions():
314                                        t = completion.type
315                                        if t == 'class':
316                                            t = '1'
317
318                                        elif t == 'function':
319                                            t = '2'
320
321                                        elif t == 'import':
322                                            t = '0'
323
324                                        elif t == 'keyword':
325                                            continue  # Keywords are already handled in PyDev
326
327                                        elif t == 'statement':
328                                            t = '3'
329
330                                        else:
331                                            t = '-1'
332
333                                        # gen list(tuple(name, doc, args, type))
334                                        lst.append((completion.name, '', '', t))
335                                    self.send(self.get_completions_message('empty', lst))
336
337                            elif data.startswith(MSG_SEARCH):
338                                data = data[len(MSG_SEARCH):]
339                                data = unquote_plus(data)
340                                (f, line, col), foundAs = _pydev_imports_tipper.search_definition(data)
341                                self.send(self.get_completions_message(f, [(line, col, foundAs)]))
342
343                            elif data.startswith(MSG_CHANGE_DIR):
344                                data = data[len(MSG_CHANGE_DIR):]
345                                data = unquote_plus(data)
346                                complete_from_dir(data)
347                                self.send(MSG_OK)
348
349                            else:
350                                self.send(MSG_INVALID_REQUEST)
351                    except Exit:
352                        e = sys.exc_info()[1]
353                        msg = self.get_completions_message(None, [('Exit:', 'SystemExit', '')])
354                        try:
355                            self.send(msg)
356                        except socket.error:
357                            pass # Ok, may be closed already
358
359                        raise e # raise original error.
360
361                    except:
362                        dbg(SERVER_NAME + ' exception occurred', ERROR)
363                        s = StringIO.StringIO()
364                        traceback.print_exc(file=s)
365
366                        err = s.getvalue()
367                        dbg(SERVER_NAME + ' received error: ' + str(err), ERROR)
368                        msg = self.get_completions_message(None, [('ERROR:', '%s\nLog:%s' % (err, log.get_contents()), '')])
369                        try:
370                            self.send(msg)
371                        except socket.error:
372                            pass # Ok, may be closed already
373
374
375                finally:
376                    log.clear_log()
377
378            self.socket.close()
379            self.ended = True
380            raise Exit()  # connection broken
381
382
383        except Exit:
384            if self.exit_process_on_kill:
385                sys.exit(0)
386            # No need to log SystemExit error
387        except:
388            s = StringIO.StringIO()
389            exc_info = sys.exc_info()
390
391            traceback.print_exception(exc_info[0], exc_info[1], exc_info[2], limit=None, file=s)
392            err = s.getvalue()
393            dbg(SERVER_NAME + ' received error: ' + str(err), ERROR)
394            raise
395
396
397
398if __name__ == '__main__':
399
400    port = int(sys.argv[1])  # this is from where we want to receive messages.
401
402    t = CompletionServer(port)
403    dbg(SERVER_NAME + ' will start', INFO1)
404    t.run()
405