示例#1
0
class Driver(threading.Thread):
    """
    Out-of-process codeintel2 driver
    This class implements the main loop of the codeintel worker process
    This should be a singleton object
    """

    # static
    _instance = None
    _command_handler_map = collections.defaultdict(list)
    """Registered command handlers; the key is the command name (a str), and the
    value is a list of command handler instances or (for lazy handlers) a
    single-element tuple containing the callable to get the real handler."""
    _default_handler = None
    """The default (core) command handler; instance of a CoreHandler"""
    _builtin_commands = {}
    """Built-in commands that cannot be overridden"""

    def __init__(self, db_base_dir=None, fd_in=sys.stdin, fd_out=sys.stdout):
        threading.Thread.__init__(self, name="Codeintel OOP Driver")
        assert Driver._instance is None, "Driver should be a singleton"
        Driver._instance = self
        logging.root.addHandler(LoggingHandler(self))
        self.daemon = True

        self.fd_in = fd_in
        self.fd_out = fd_out
        self.abort = None
        self.quit = False
        self.buffers = {} # path to Buffer objects
        self.next_buffer = 0
        self.active_request = None

        self.send_queue = Queue.Queue()
        self.send_thread = threading.Thread(name="Codeintel OOP Driver Send Thread",
                                            target=self._send_proc)
        self.send_thread.daemon = True
        self.send_thread.start()

        self.queue = collections.deque()
        self.queue_cv = threading.Condition()
        self.env = Environment(name="global",
                               send_fn=functools.partial(self.send, request=None))

        # Fill out the non-overridable build-in commands
        self._builtin_commands = {}
        for attr in dir(self):
            # Note that we check startswith first to avoid getters etc.
            if attr.startswith("do_") and callable(getattr(self, attr)):
                command = attr[len("do_"):].replace("_", "-")
                self._builtin_commands[command] = getattr(self, attr)

        from codeintel2.manager import Manager
        log.debug("using db base dir %s", db_base_dir)
        self.mgr = Manager(db_base_dir=db_base_dir,
                           db_catalog_dirs=[],
                           db_event_reporter=self._DBEventReporter(self),
                           env=self.env,
                           on_scan_complete=self._on_scan_complete)
        self.mgr.initialize()

    def _on_scan_complete(self, scan_request):
        if scan_request.status in ("changed", "skipped"):
            # Send unsolicited response about the completed scan
            buf = scan_request.buf
            self.send(request=None,
                      path=buf.path,
                      language=buf.lang,
                      command="scan-complete")

    class _DBEventReporter(object):
        def __init__(self, driver):
            self.driver = driver
            self.log = log.getChild("DBEventReporter")
            self.debug = self.log.debug

            # directories being scanned (completed or not)
            # key is unicode path, value is number of times it's active
            self._dirs = collections.defaultdict(int)
            # directories which are complete (unicode path)
            self._completed_dirs = set()

        def send(self, **kwargs):
            self.driver.send(request=None, command="report-message", **kwargs)

        def __call__(self, message):
            """Old-style status messages before long-running jobs
            @param msg {str or None} The message to display
            """
            if len(self._dirs):
                return # ignore old-style db events if we're doing a scan
            self.debug("db event: %s", message)
            self.send(message=message)

        def onScanStarted(self, description, dirs=set()):
            """Called when a directory scan is about to start
            @param description {unicode} A string suitable for showing the user
                about the upcoming operation
            @param dirs {set of unicode} The directories about to be scanned
            """
            self.debug("scan started: %s (%s dirs)", description, len(dirs))

            assert dirs, "onScanStarted expects non-empty directories"
            if not dirs: # empty set - we shouldn't have gotten here, but be nice
                return
            for dir_path in dirs:
                self._dirs[dir_path] += 1
            self.send(type="scan-progress", message=description,
                      completed=len(self._completed_dirs),
                      total=len(self._dirs))

        def onScanDirectory(self, description, dir_path, current=None, total=None):
            """Called when a directory is being scanned (out of possibly many)
            @param description {unicode} A string suitable for showing the user
                    regarding the progress
            @param dir {unicode} The directory currently being scanned
            @param current {int} The current progress
            @param total {int} The total number of directories to scan in this
                    request
            """
            self.debug("scan directory: %s (%s %s/%s)",
                      description, dir_path, current, total)

            assert dir_path, "onScanDirectory got no directory"
            if dir_path:
                self._completed_dirs.add(dir_path)
            self.send(type="scan-progress", message=description,
                      completed=len(self._completed_dirs),
                      total=len(self._dirs))

        def onScanComplete(self, dirs=set(), scanned=set()):
            """Called when a scan operation is complete
            @param dirs {set of unicode} The directories that were intially
                   requested to be scanned (as pass in onScanStarted)
            @param scanned {set of unicode} Directories which were successfully
                   scanned.  This may be a subset of dirs if the scan was
                   aborted.
            """
            self.debug("scan complete: scanned %r/%r dirs",
                      len(scanned), len(dirs))

            for dir_path in dirs:
                self._dirs[dir_path] -= 1
                if not self._dirs[dir_path]:
                    del self._dirs[dir_path]
                    self._completed_dirs.discard(dir_path)
            self.send(type="scan-progress", completed=len(self._completed_dirs),
                      total=len(self._dirs))

    REQUEST_DEFAULT = object()

    def send(self, request=REQUEST_DEFAULT, **kwargs):
        """
        Send a response
        """
        data = dict(kwargs)
        if request is Driver.REQUEST_DEFAULT:
            request = self.active_request
        if request:
            data["req_id"] = request.id
        if "success" not in data:
            data["success"] = True
        elif data["success"] is None:
            del data["success"]
        buf = json.dumps(data, separators=(',',':'))
        buf_len = str(len(buf))
        log.debug("sending: %s:[%s]", buf_len, buf)
        self.send_queue.put(buf)

    def _send_proc(self):
        while True:
            buf = self.send_queue.get()
            try:
                buf_len = str(len(buf))
                self.fd_out.write(buf_len)
                self.fd_out.write(buf)
            finally:
                self.send_queue.task_done()

    def fail(self, request=REQUEST_DEFAULT, **kwargs):
        kwargs = kwargs.copy()
        if not "command" in kwargs and request:
            try:
                kwargs["command"] = request["command"]
            except KeyError:
                pass
        return self.send(request=request, success=False, **kwargs)

    def exception(self, request=REQUEST_DEFAULT, **kwargs):
        return self.fail(request=request, stack=traceback.format_exc(), **kwargs)

    @staticmethod
    def normpath(path):
        """Routine to normalize the path used for codeintel buffers
        This is annoying because it needs to handle unsaved files, as well as
        urls.
        @note See also koCodeIntel.py::KoCodeIntelBuffer.normpath
        """
        if path.startswith("<Unsaved>"):
            return path # Don't munge unsaved file paths

        scheme = path.split(":", 1)[0]

        if len(scheme) == len(path):
            # didn't find a scheme at all; assume this is a local path
            return os.path.normcase(path)
        if len(scheme) == 1:
            # single-character scheme; assume this is actually a drive letter
            # (for a Windows-style path)
            return os.path.normcase(path)
        scheme_chars = string.ascii_letters + string.digits + "-"
        try:
            scheme = scheme.encode("ascii")
        except UnicodeEncodeError:
            # scheme has a non-ascii character; assume local path
            return os.path.normcase(path)
        if scheme.translate(None, scheme_chars):
            # has a non-scheme character: this is not a valid scheme
            # assume this is a local file path
            return os.path.normcase(path)
        if scheme != "file":
            return path # non-file scheme
        path = path[len(scheme) + 1:]
        return os.path.normcase(path)

    def get_buffer(self, request=REQUEST_DEFAULT, path=None):
        if request is Driver.REQUEST_DEFAULT:
            request = self.active_request
        if path is None:
            if not "path" in request:
                raise RequestFailure(message="No path given to locate buffer")
            path = request.path
        path = self.normpath(path)
        try:
            buf = self.buffers[path]
        except KeyError:
            buf = None
        else:
            if "language" in request and buf.lang != request.language:
                buf = None # language changed, re-scan

        if not buf:
            # Need to construct a new buffer
            lang = request.get("language")
            env = Environment(request=request, name=os.path.basename(path))
            lexer = self.mgr.silvercity_lexer_from_lang.get(lang)
            accessor = codeintel2.accessor.OOPAccessor(lexer, self)
            buf = self.mgr.buf_from_accessor(accessor, lang, path=path, env=env)
            accessor.buf = buf

        if "checksum" in request:
            buf.accessor.checksum = request.get("checksum")

        if request.get("text") is not None:
            # text came with the buffer; most likely it's a unit test...
            buf.accessor.reset_content(request.get("text"))
            buf.encoding = "utf-8"

        try:
            env = request["env"]
        except KeyError:
            pass # no environment, use current
        else:
            buf._env.update(env)

        #log.debug("Got buffer %r: [%s]", buf, buf.accessor.content)
        log.debug("Got buffer %r", buf)

        self.buffers[path] = buf
        return buf

    def sync_get_buffer_contents(self, path, checksum):
        """Synchronously attempt to get the buffer contents from the parent
        process.  This will block the code intel process until the response has
        been obtained.
        @param path {str or unicode} The path of the buffer
        @param checksum {str} The checksum of the current contents (md5)
        @return {dict} The response from the parent process
        """
        self.send(request=None,
                  command="get-buffer-contents",
                  checksum=checksum,
                  path=path)

        # Wait synchronously for the response to come back
        buf_req = None
        with self.queue_cv:
            while buf_req is None:
                for i, buf_req in enumerate(self.queue):
                    if (buf_req.get("command") == "get-buffer-contents"
                            and buf_req.get("path") == path):
                        del self.queue[i]
                        break
                else:
                    buf_req = None
                    log.debug("Skipping wake, waiting for more...")
                    self.queue_cv.wait()

        if not buf_req.get("success", False):
            log.debug("Failed to get text for %s", path)
            raise RequestFailure(msg="Failed to get buffer %s" % (path,))

        log.debug("Got text for %s", path)
        return buf_req

    def do_abort(self, request):
        try:
            req_id = request["id"]
            with self.queue_cv:
                for item in self.queue:
                    if queue.get("req_id") == req_id:
                        self.queue.remove(queue)
                        self.send(request=request)
                        break
                else:
                    self.abort = req_id
                    if self.active_request and self.active_request.id == req_id:
                        # need to wait a bit...
                        self.send(request=request)
                    else:
                        self.fail(request=request,
                                  message="Request %s not found" % (req_id,))
        except RequestFailure as e:
            self.fail(request=request, **e.kwargs)
        except Exception as e:
            log.exception(e.message)
            self.exception(request=request, message=e.message)

    def do_add_dirs(self, request):
        catalog_dirs = request.get("catalog-dirs", None)
        if catalog_dirs is not None:
            self.mgr.db.catalog_dirs.extend(catalog_dirs)
            catalog_dirs = self.mgr.db.catalog_dirs
        lexer_dirs = request.get("lexer-dirs", [])
        codeintel2.udl.UDLLexer.add_extra_lexer_dirs(lexer_dirs)
        module_dirs = request.get("module-dirs", [])
        if module_dirs:
            self.mgr._register_modules(module_dirs)
        if catalog_dirs is not None:
            self.mgr.db.get_catalogs_zone().catalog_dirs = catalog_dirs
        self.send(request=request)

    def do_load_extension(self, request):
        """Load an extension that, for example, might provide additional
        command handlers"""
        name = request.get("module-name", None)
        if not name:
            raise RequestFailure(msg="load-extension requires a module-name")
        path = request.get("module-path", None)
        names = name.split(".")
        if len(names) > 1:
            # It's inside a package - go look for the package(s).
            for package_name in names[:-1]:
                iinfo = imp.find_module(package_name, [path] if path else None)
                if not iinfo:
                    raise RequestFailure(msg="load-extension could not find "
                                             "package %r for given name %r"
                                             % (package_name, name))
                path = iinfo[1]
            name = names[-1]
        iinfo = imp.find_module(name, [path] if path else None)
        try:
            module = imp.load_module(name, *iinfo)
        finally:
            if iinfo and iinfo[0]:
                iinfo[0].close()
        callback = getattr(module, "registerExtension", None)
        if not callback:
            raise RequestFailure(msg="load-extension module %s should "
                                     "have a 'registerExtension' method "
                                     "taking no arguments" % (name,))
        callback()
        self.send() # success, finally

    def do_quit(self, request):
        self.quit = True
        self.send(command="quit")

    def report_error(self, message):
        self.send(request=None,
                  command="report-error",
                  message=unicode(message))

    def start(self):
        """Start reading from the socket and dump requests into the queue"""
        log.info("Running codeintel driver...")
        buf = ""
        self.send(success=None)
        self.daemon = True
        threading.Thread.start(self)
        while not self.quit:
            try:
                ch = self.fd_in.read(1)
            except IOError:
                log.debug("Failed to read frame length, assuming connection died")
                self.quit = True
                break
            if len(ch) == 0:
                log.debug("Input was closed")
                self.quit = True
                break
            if ch == "{":
                size = int(buf, 10)
                try:
                    buf = ch + self.fd_in.read(size - 1) # exclude already-read {
                except IOError:
                    log.debug("Failed to read frame data, assuming connection died")
                    self.quit = True
                    break
                try:
                    data = json.loads(buf)
                    request = Request(data)
                except Exception as e:
                    log.exception(e)
                    self.exception(message=e.message, request=None)
                    continue
                finally:
                    buf = ""
                if request.get("command") == "abort":
                    self.do_abort(request=request)
                else:
                    log.debug("queuing request %r", request)
                    with self.queue_cv:
                        self.queue.appendleft(request)
                        self.queue_cv.notify()
            elif ch in "0123456789":
                buf += ch
            else:
                raise ValueError("Invalid request data: " + ch.encode("hex"))

    def run(self):
        """Evaluate and send results back"""
        log.info("Running codeintel eval thread...")
        buf = ""
        log.debug("default supported commands: %s",
                  ", ".join(self._default_handler.supportedCommands))
        while True:
            with self.queue_cv:
                try:
                    request = self.queue.pop()
                except IndexError:
                    self.queue_cv.wait()
                    continue
            log.debug("doing request %r", request)
            try:
                self.active_request = request
                command = request.command
                # First, check abort and quit; don't allow those to be overridden
                try:
                    builtin = self._builtin_commands[command]
                except KeyError:
                    pass
                else:
                    builtin(request)
                    continue
                handlers = self._command_handler_map.get(command, [])[:]
                if command in self._default_handler.supportedCommands:
                    # The default handler can deal with this, put it at the end
                    handlers.append(self._default_handler)
                for handler in handlers:
                    if isinstance(handler, tuple):
                        try:
                            real_handler = handler[0]()
                        except Exception as ex:
                            log.exception("Failed to get lazy handler for %s", command)
                            real_handler = None
                        if real_handler is None:
                            # Handler failed to instantiate, drop it
                            try:
                                self._command_handler_map[command].remove(handler)
                            except ValueError:
                                pass # ... shouldn't happen, but tolerate it
                            continue
                        for handlers in self._command_handler_map.values():
                            try:
                                handlers[handlers.index(handler)] = real_handler
                            except ValueError:
                                pass # handler not in this list
                        handler = real_handler
                    if handler.canHandleRequest(request):
                        handler.handleRequest(request, self)
                        break
                else:
                    self.fail(request=request,
                              msg="Don't know how to handle command %s" % (command,))

            except RequestFailure as e:
                self.fail(request=request, **e.kwargs)
            except Exception as e:
                log.exception(e.message)
                self.exception(request=request, message=e.message)
            finally:
                self.active_request = None

    @classmethod
    def registerCommandHandler(cls, handlerInstance):
        """Register a command handler"""
        for command in handlerInstance.supportedCommands:
            cls._command_handler_map[command].append(handlerInstance)

    @classmethod
    def registerLazyCommandHandler(cls, supported_commands, constructor):
        """Register a lazy command handler
        @param supported_commands {iterable} The commands to handle; each
            element should be a str of the command name.
        @param constructor {callable} Function to be called to get the real
            command handler; it should take no arguments and return a command
            handler instance.  It may return None if the command is not
            available; it will not be asked again.
        """
        for command in supported_commands:
            cls._command_handler_map[command].append((constructor,))

    @classmethod
    def getInstance(cls):
        """Get the singleton instance of the driver"""
        return Driver._instance