class Process(object):
    '''
    Process class
    This class represents a managed process.
    '''

    def __init__(self):
        self._binary = None
        self._args = None
        self._env = None
        self._cwd = None
        self._filehandles = FileHandles()

        self._handle = None

    @property
    def binary(self):
        ''' Returns the process binary. '''
        return self._binary

    @binary.setter
    def binary(self, binary):
        ''' Sets the process binary. '''
        assert isinstance(binary, str), 'binary must be a string: %r' % binary

        resolved_binary = resolve_binary_path(binary)
        if resolved_binary is None:
            raise RuntimeError('failed to locate binary: %r' % (binary))

        assert is_file(resolved_binary), 'binary path invalid: %r' % (binary)

        logger.debug('setting binary to: %s', resolved_binary)
        self._binary = resolved_binary

    @property
    def args(self):
        ''' Returns the process args. Initializes it if it is None. '''
        if self._args is None:
            self._args = []
        return self._args

    @args.setter
    def args(self, args):
        ''' Sets the process arguments. '''
        if self.alive():
            logger.warning('process already started... no point setting args')

        assert hasattr(args, '__iter__'), 'args must be iterable: %r' % args
        # collect the arguments, so we don't exhaust the iterator
        args = list(args)

        if self._args is not None:
            logger.warning('overwriting existing process args: %r', self._args)
        logger.debug('setting args to: %s', args)

        self._args = list(args)

    @property
    def env(self):
        ''' Returns the process env. Initializes it if it is `None`. '''
        if self._env is None:
            self._env = {}
        return self._env

    @env.setter
    def env(self, env):
        ''' Sets the process environment variables. '''
        if self.alive():
            logger.warning('process already started... no point setting env')

        assert isinstance(env, dict), 'env must be a dictionary: %r' % env

        if self._env is not None:
            logger.warning('overwriting existing process env: %r', self._env)
        logger.debug('setting env to: %s', env)

        self._env = dict(env)

    @property
    def cwd(self):
        ''' Returns the process working directory. '''
        return self._cwd

    @cwd.setter
    def cwd(self, cwd):
        ''' Sets the process working directory. '''
        if self.alive():
            logger.warning('process already started... no point setting cwd')

        assert isinstance(cwd, str), 'cwd must be a string: %r' % cwd
        if not is_directory(cwd):
            logger.warning('invalid working directory: %s', cwd)

        logger.debug('setting cwd to: %s', cwd)
        self._cwd = cwd

    @property
    def filehandles(self):
        ''' Returns the process file handles. '''
        return self._filehandles

    def alive(self):
        ''' Returns whether or not the process is active. '''
        if self._handle is None:
            return False
        assert isinstance(self._handle, subprocess.Popen), \
            '[internal] handle is not a Popen instance: %r' % self._handle
        status = self._handle.poll()

        status_desc = 'alive' if status is None else 'exited (%r)' % (status)
        logger.debug('process handle status: %s', status_desc)

        return status is None

    def start(self):
        ''' Starts the process according to current configuration. '''
        if self.alive():
            raise Exception('process has already been started')

        assert self._binary is not None and isinstance(self._binary, str), \
            'process binary is invalid: %r' % self._binary
        assert self._args is None or hasattr(self._args, '__iter__'), \
            'process args list is invalid: %r' % self._args

        process_args = [self._binary]
        if self._args:
            process_args.extend(self._args)

        popen_args = {
            'args': process_args,
            'stdin': self._filehandles.stdin,
            'stdout': self._filehandles.stdout,
            'stderr': self._filehandles.stderr,
            'cwd': self._cwd,
            'env': self._env,
        }
        logger.debug('arguments to Popen are: %r', popen_args)

        self._handle = subprocess.Popen(**popen_args)

    def communicate(self, inpt=None, timeout=None):
        '''
        Sends data via stdin and reads data from stdout, stderr.
        This will likely block if the process is still alive.
        When `inpt` is `None`, stdin is immediately closed.
        When `timeout` is `None`, this waits indefinitely for the process to
        terminate. Otherwise, it is interpreted as the number of seconds to
        wait for until raising a `TimeoutExpired` exception.
        '''
        if not self.alive():
            logger.debug('process not alive, unlikely to block')

        assert self._handle is not None, '[internal] process handle is null'
        return self._handle.communicate(inpt, timeout)

    def wait(self, timeout=10):
        ''' Waits `timeout` seconds for the process to finish. '''
        if not self.alive():
            logger.debug('process not alive, nothing to wait for')
            return

        assert self._handle is not None, '[internal] process handle is null'

        try:
            self._handle.wait(timeout=timeout)
        except subprocess.TimeoutExpired as e:
            # re-raise as `TimeoutError` to keep things consistent
            raise TimeoutError(e)

    def kill(self):
        ''' Kills the associated process by sending a signal. '''
        if not self.alive():
            logger.debug('process is already dead, not sending signal')
            return

        assert self._handle is not None, '[internal] process handle is null'
        self._handle.kill()

    def pid(self):
        ''' Returns the process ID if running, or `None` otherwise. '''
        if not self.alive():
            logger.debug('process not alive, no pid')
            return None

        return self._handle.pid def parse_completions(json, request_parameters=None):
    '''
    Wrapper around `parse_compoptions` and `parse_diagnostics`. Uses both to
    generate and return a `CompletionResponse`.
    '''

    def _attempt_parse(parser, *args, **kwargs):
        try:
            return parser(*args, **kwargs)
        except ValueError as e:
            logger.warning('response has unexpected format: %r', e)
        except NotImplementedError as e:
            logger.warning('unhandled response format: %r', e)
        except Exception as e:
            logger.error('error while parsing response: %r', e)
        return None

    completions = _attempt_parse(parse_compoptions, json, request_parameters)
    diagnostics = _attempt_parse(parse_diagnostics, json, request_parameters)

    return CompletionResponse(
        completions=completions, diagnostics=diagnostics,
    ) class RequestParameters(object):
    '''
    Wrapper around json parameters used in ycmd requests. Supports arbitrary
    extra parameters using a `dict`-like interface.
    '''

    def __init__(self, file_path=None, file_contents=None, file_types=None,
                 line_num=None, column_num=None, force_semantic=None):
        # copy-paste of reset:
        self._file_path = None
        self._file_contents = None
        self._file_types = None
        self._line_num = None
        self._column_num = None
        self._force_semantic = None
        self._extra_params = {}

        self.file_path = file_path
        self.file_contents = file_contents
        self.file_types = file_types
        self.line_num = line_num
        self.column_num = column_num
        self.force_semantic = force_semantic

    def reset(self):
        ''' Deletes all stored parameters. '''
        self._file_path = None
        self._file_contents = None
        self._file_types = None
        self._line_num = None
        self._column_num = None
        self._force_semantic = None
        self._extra_params = {}

    def to_json(self):
        '''
        Generates and returns a `dict` representing all stored parameters, for
        use in sending the request.
        This will additionally validate all parameters, and generate defaults
        for any missing ones.
        '''
        file_path = self.file_path
        file_contents = self.file_contents
        file_types = self.file_types
        line_num = self.line_num
        column_num = self.column_num
        extra_params = self._extra_params
        force_semantic = self._force_semantic

        # validate
        if not file_path:
            raise ValueError('no file path specified')
        if not isinstance(file_path, str):
            raise TypeError('file path must be a str: %r' % (file_path))

        if not file_contents:
            file_contents = ''
        if not isinstance(file_contents, str):
            raise TypeError(
                'file contents must be a str: %r' % (file_contents)
            )

        if file_types is None:
            file_types = []
        if not isinstance(file_types, (tuple, list)):
            raise TypeError('file types must be a list: %r' % (file_types))

        if line_num is None:
            line_num = 1
        if not isinstance(line_num, int):
            raise TypeError('line num must be an int: %r' % (line_num))

        if column_num is None:
            column_num = 1
        if not isinstance(column_num, int):
            raise TypeError('column num must be an int: %r' % (column_num))

        optional_params = {}
        if force_semantic is not None:
            if not isinstance(force_semantic, bool):
                raise TypeError(
                    'force-semantic must be a bool: %r' % (force_semantic)
                )

            optional_params['force_semantic'] = force_semantic

        if extra_params is None:
            extra_params = {}
        if not isinstance(extra_params, dict):
            raise TypeError(
                'extra parameters must be a dict: %r' % (extra_params)
            )

        json_params = {
            'filepath': file_path,
            'file_data': {
                file_path: {
                    'filetypes': file_types,
                    'contents': file_contents,
                },
            },

            'line_num': line_num,
            'column_num': column_num,
        }
        json_params.update(optional_params)
        json_params.update(extra_params)

        return json_params

    @property
    def file_path(self):
        if not self._file_path:
            logger.warning('no file path set')
            return ''
        return self._file_path

    @file_path.setter
    def file_path(self, file_path):
        if file_path is not None and not isinstance(file_path, str):
            raise TypeError
        self._file_path = file_path

    @property
    def file_contents(self):
        if not self._file_contents:
            logger.warning('no file contents set')
            return ''
        return self._file_contents

    @file_contents.setter
    def file_contents(self, file_contents):
        if file_contents is not None and not isinstance(file_contents, str):
            raise TypeError
        self._file_contents = file_contents

    @property
    def file_types(self):
        if not self._file_types:
            logger.warning('no file types set')
            return []
        return self._file_types

    @file_types.setter
    def file_types(self, file_types):
        if isinstance(file_types, str):
            file_types = [file_types]
        if file_types is not None and \
                not isinstance(file_types, (tuple, list)):
            raise TypeError
        # create a shallow copy
        self._file_types = list(file_types)

    @property
    def line_num(self):
        if not self._line_num:
            logger.warning('no line number set')
            return 1
        return self._line_num

    @line_num.setter
    def line_num(self, line_num):
        if line_num is not None and not isinstance(line_num, int):
            raise TypeError
        if line_num <= 0:
            raise ValueError
        self._line_num = line_num

    @property
    def column_num(self):
        if not self._column_num:
            logger.warning('no column number set')
            return 1
        return self._column_num

    @column_num.setter
    def column_num(self, column_num):
        if column_num is not None and not isinstance(column_num, int):
            raise TypeError
        if column_num <= 0:
            raise ValueError
        self._column_num = column_num

    @property
    def force_semantic(self):
        return self._force_semantic

    @force_semantic.setter
    def force_semantic(self, force_semantic):
        if force_semantic is not None and not isinstance(force_semantic, bool):
            raise TypeError
        self._force_semantic = force_semantic

    def __getitem__(self, key):
        ''' Retrieves `key` from the extra parameters. '''
        if self._extra_params is None:
            self._extra_params = {}
        return self._extra_params[key]

    def get(self, key, default=None):
        '''
        Retrieves `key` from the extra parameters. Returns `default` if unset.
        '''
        if self._extra_params is None:
            self._extra_params = {}
        return self._extra_params.get(key, default)

    def __setitem__(self, key, value):
        '''
        Sets `key` in the extra parameters. These parameters have higher
        priority than the file-based parameters, and may overwrite them if the
        same key is used.
        '''
        if self._extra_params is None:
            self._extra_params = {}
        self._extra_params[key] = value

    def __delitem__(self, key):
        ''' Clears the `key` extra parameter. '''
        if self._extra_params is None:
            return
        del self._extra_params[key]

    def __iter__(self):
        ''' Dictionary-compatible iterator. '''
        base_items = [
            ('file_path', self._file_path),
            ('file_contents', self._file_contents),
            ('file_types', self._file_types),
            ('line_num', self._line_num),
            ('column_num', self._column_num),
        ]
        if not self._extra_params:
            return iter(base_items)

        extra_items = self._extra_params.items()

        all_items = list(base_items) + list(extra_items)
        return iter(all_items)

    def __str__(self):
        return str(dict(self))

    def __repr__(self):
        return '%s(%r)' % ('RequestParameters', dict(self)) def json_serialize(data):
    ''' Serializes `data` from a `dict` to a json `str`. '''
    assert isinstance(data, dict), 'data must be a dict: %r' % (data)
    serialized = json.dumps(data)
    return serialized def json_parse(data):
    ''' Parses `data` from a json `str` a `dict`. '''
    assert isinstance(data, (str, bytes)), \
        'data must be str or bytes: %r' % (data)
    if isinstance(data, bytes):
        data = bytes_to_str(data)

    parsed = json.loads(data)
    return parsed def is_file(path):
    '''
    Returns true if the supplied `path` refers to a valid plain file, and
    false otherwise.
    '''
    if not isinstance(path, str):
        raise TypeError('path must be a str: %r' % (path))
    return os.path.exists(path) and os.path.isfile(path) def get_base_name(path):
    '''
    Returns the base name for the file at `path`. If `path` refers to a
    directory, the directory name is returned. If `path` refers to a mount
    point, the base name will be `None`.
    '''
    if not isinstance(path, str):
        raise TypeError('path must be a str: %r' % (path))
    head, tail = os.path.split(path)
    if tail:
        return tail
    if head != path:
        # stripped a trailing directory separator, so redo it
        head, tail = os.path.split(head)

    return tail def calculate_hmac(hmac_secret, *content, digestmod=hashlib.sha256):
    '''
    Calculates the HMAC for the given `content` using the `hmac_secret`.
    This is calculated by first generating the HMAC for each item in
    `content` separately, then concatenating them, and finally running another
    HMAC on the concatenated intermediate result. Finally, the result of that
    is base-64 encoded, so it is suitable for use in headers.
    '''
    assert isinstance(hmac_secret, (str, bytes)), \
        'hmac secret must be str or bytes: %r' % (hmac_secret)
    if len(content) == 0:
        raise ValueError('no data supplied')

    hmac_secret = str_to_bytes(hmac_secret)

    content_hmac_digests = map(
        lambda data: _calculate_hmac(
            hmac_secret, data=data, digestmod=digestmod,
        ), content,
    )

    if len(content) > 1:
        # compose individual hmac fragments into one and then hash it again
        concatenated_hmac_digests = b''.join(content_hmac_digests)
        hmac_digest_binary = _calculate_hmac(
            hmac_secret, data=concatenated_hmac_digests, digestmod=digestmod,
        )
    else:
        assert len(content) == 1, \
            '[internal] number of arguments is not 1: %r' % (content)
        hmac_digest_binary = next(content_hmac_digests)

    hmac_digest_bytes = base64_encode(hmac_digest_binary)
    hmac_digest_str = bytes_to_str(hmac_digest_bytes)

    return hmac_digest_str def new_hmac_secret(num_bytes=32):
    ''' Generates and returns an HMAC secret in binary encoding. '''
    hmac_secret_binary = os.urandom(num_bytes)
    return hmac_secret_binary def lock_guard(lock=None):
    '''
    Locking decorator.

    Calls the decorated function with the `lock` held, and releases when done.

    If `lock` is omitted, and a class method is passed in, the decorated method
    will attempt to use the instance-specific `self._lock` variable. If the
    instance has no lock, or if a static function is decorated, a unique
    `threading.RLock` will be created.
    '''

    _lock = lock if lock is not None else threading.RLock()

    if not hasattr(_lock, '__enter__') or not hasattr(_lock, '__exit__'):
        raise TypeError('lock must support context management: %r' % (_lock))

    def lock_guard_function(fn):

        if hasattr(fn, '__self__'):
            # given a method, so attempt to use an instance-specific lock
            @functools.wraps(fn)
            def lock_guard_run(self, *args, **kwargs):
                __lock = getattr(self, '_lock', _lock)
                with __lock:
                    return fn(*args, **kwargs)
        else:
            # given a function, always use the default lock
            @functools.wraps(fn)
            def lock_guard_run(*args, **kwargs):
                __lock = _lock
                with __lock:
                    return fn(*args, **kwargs)

        return lock_guard_run

    return lock_guard_function def str_to_bytes(data):
    '''
    Converts `data` to `bytes`.

    If data is a `str`, it is encoded into `bytes`.
    If data is `bytes`, it is returned as-is.
    '''
    assert isinstance(data, (str, bytes)), \
        'data must be str or bytes: %r' % (data)
    if isinstance(data, bytes):
        # already bytes, yay
        return data
    return data.encode() def truncate(data, max_sz=16):
    '''
    Truncates the input `data` in a somewhat-intelligent way. The purpose of
    this function is to control the output size of logger statements when given
    really long strings.
    When `data` is a `str` or `bytes`, it is truncated to `max_sz` and the last
    3 characters are replaced with '.' (e.g. 'long string' -> 'long...').
    When `data` is a `list` or `dict`, each entry is truncated recursively. Any
    string values will be truncated as above. For dictionaries, keys will never
    get truncated, only their values.
    '''
    if data is None:
        return data

    def _truncate_str(data=data, max_sz=max_sz):
        assert isinstance(data, (str, bytes)), \
            'data must be str or bytes: %r' % (data)
        assert isinstance(max_sz, int), \
            'max size must be an int: %r' % (max_sz)
        assert max_sz > 8, 'max size must be at least 8: %r' % (max_sz)

        data_sz = len(data)
        if data_sz <= max_sz:
            # already short enough
            return data

        if isinstance(data, str):
            ellip = '...'
        elif isinstance(data, bytes):
            ellip = b'...'
        else:
            ellip = None

        ellip_sz = len(ellip) if ellip else 0
        remaining_sz = max_sz - ellip_sz
        assert remaining_sz > 0, \
            '[internal] remaining size is not positive: %r' % (remaining_sz)

        truncated = data[:remaining_sz]
        if ellip:
            truncated += ellip

        return truncated

    def _truncate_collection(data=data, max_sz=max_sz):
        assert hasattr(data, '__iter__'), \
            'data must be list or dict: %r' % (data)
        # don't actually care about max size here, so no need to type check it

        if hasattr(data, 'items') and callable(data.items):
            collection_iter = data.items()
            result = {}

            def add_result(k, v):
                result[k] = v
        else:
            collection_iter = enumerate(data)
            result = []

            def add_result(_, v):
                result.append(v)

        for k, v in collection_iter:
            truncated = truncate(data=v, max_sz=max_sz)
            add_result(k, truncated)

        return result

    if isinstance(data, (str, bytes)):
        return _truncate_str(data=data, max_sz=max_sz)
    elif hasattr(data, '__iter__'):
        return _truncate_collection(data=data, max_sz=max_sz)

    try:
        data_str = str(data)
    except ValueError:
        # don't know... return data as-is
        return data

    return _truncate_str(data=data_str, max_sz=max_sz) def get_unused_port(interface='127.0.0.1'):
    ''' Finds an available port for a server process to listen on. '''
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind((interface, 0))

    port = sock.getsockname()[1]
    logger.debug('found unused port: %d', port)

    sock.close()
    return port YCMD_EVENT_BUFFER_UNLOAD = 'BufferUnload' YCMD_EVENT_BUFFER_VISIT = 'BufferVisit' YCMD_EVENT_CURRENT_IDENTIFIER_FINISHED = 'CurrentIdentifierFinished' YCMD_EVENT_FILE_READY_TO_PARSE = 'FileReadyToParse' YCMD_EVENT_INSERT_LEAVE = 'InsertLeave' YCMD_HANDLER_DEBUG_INFO = '/debug_info' YCMD_HANDLER_DEFINED_SUBCOMMANDS = '/defined_subcommands' YCMD_HANDLER_EVENT_NOTIFICATION = '/event_notification' YCMD_HANDLER_GET_COMPLETIONS = '/completions' YCMD_HANDLER_HEALTHY = '/healthy' YCMD_HANDLER_IGNORE_EXTRA_CONF = '/ignore_extra_conf_file' YCMD_HANDLER_LOAD_EXTRA_CONF = '/load_extra_conf_file' YCMD_HANDLER_SHUTDOWN = '/shutdown' YCMD_HMAC_HEADER = 'X-Ycm-Hmac' YCMD_HMAC_SECRET_LENGTH = 16 class StartupParameters(object):
    '''
    Startup parameters for a ycmd server instance.
    Should include all the necessary configuration for creating the ycmd
    server process. Also calculates defaults for certain parameters.
    '''

    def __init__(self, ycmd_root_directory=None,
                 ycmd_settings_path=None,
                 working_directory=None,
                 python_binary_path=None,
                 server_idle_suicide_seconds=None,
                 server_check_interval_seconds=None):
        self._ycmd_root_directory = None
        self._ycmd_settings_path = None

        self._working_directory = None
        self._python_binary_path = None
        self._server_idle_suicide_seconds = None
        self._server_check_interval_seconds = None

        # additional attributes, can be set via the properties
        self._log_level = None
        self._stdout_log_path = None
        self._stderr_log_path = None
        self._keep_logs = None

        self.ycmd_root_directory = ycmd_root_directory
        self.ycmd_settings_path = ycmd_settings_path
        self.working_directory = working_directory
        self.python_binary_path = python_binary_path
        self.server_idle_suicide_seconds = server_idle_suicide_seconds
        self.server_check_interval_seconds = server_check_interval_seconds

    @property
    def ycmd_root_directory(self):
        if self._ycmd_root_directory is None:
            logger.warning('no ycmd root directory has been set')
        return self._ycmd_root_directory

    @ycmd_root_directory.setter
    def ycmd_root_directory(self, ycmd_root_directory):
        if ycmd_root_directory is not None and \
                not isinstance(ycmd_root_directory, str):
            raise TypeError(ycmd_root_directory,)
        self._ycmd_root_directory = ycmd_root_directory

    @property
    def ycmd_settings_path(self):
        if self._ycmd_settings_path is None:
            if self._ycmd_root_directory is not None:
                return get_default_settings_path(self._ycmd_root_directory)
            logger.warning('no ycmd root directory has been set')

        return self._ycmd_settings_path

    @ycmd_settings_path.setter
    def ycmd_settings_path(self, ycmd_settings_path):
        if ycmd_settings_path is not None and \
                not isinstance(ycmd_settings_path, str):
            raise TypeError(ycmd_settings_path,)
        self._ycmd_settings_path = ycmd_settings_path

    @property
    def working_directory(self):
        if self._working_directory is None:
            return os.getcwd()
        return self._working_directory

    @working_directory.setter
    def working_directory(self, working_directory):
        if working_directory is not None and \
                not isinstance(working_directory, str):
            raise TypeError(working_directory,)
        self._working_directory = working_directory

    @property
    def python_binary_path(self):
        if self._python_binary_path is None:
            return default_python_binary_path()
        return self._python_binary_path

    @python_binary_path.setter
    def python_binary_path(self, python_binary_path):
        if python_binary_path is not None and \
                not isinstance(python_binary_path, str):
            raise TypeError(python_binary_path,)
        self._python_binary_path = python_binary_path

    @property
    def server_idle_suicide_seconds(self):
        if self._server_idle_suicide_seconds is None:
            return YCMD_DEFAULT_SERVER_IDLE_SUICIDE_SECONDS
        return self._server_idle_suicide_seconds

    @server_idle_suicide_seconds.setter
    def server_idle_suicide_seconds(self, server_idle_suicide_seconds):
        if server_idle_suicide_seconds is not None and \
                not isinstance(server_idle_suicide_seconds, int):
            raise TypeError(server_idle_suicide_seconds,)
        self._server_idle_suicide_seconds = server_idle_suicide_seconds

    @property
    def server_check_interval_seconds(self):
        if self._server_check_interval_seconds is None:
            return YCMD_DEFAULT_SERVER_CHECK_INTERVAL_SECONDS
        return self._server_check_interval_seconds

    @server_check_interval_seconds.setter
    def server_check_interval_seconds(self, server_check_interval_seconds):
        if server_check_interval_seconds is not None and \
                not isinstance(server_check_interval_seconds, int):
            raise TypeError(server_check_interval_seconds,)
        self._server_check_interval_seconds = server_check_interval_seconds

    @property
    def log_level(self):
        return self._log_level

    @log_level.setter
    def log_level(self, log_level):
        if log_level is not None and not isinstance(log_level, str):
            raise TypeError('log level must be a str: %r' % (log_level))

        if log_level is not None and not _is_valid_log_level(log_level):
            logger.warning('log level unrecognized: %r', log_level)
            # but fall through and do it anyway

        self._log_level = log_level

    @property
    def stdout_log_path(self):
        return self._stdout_log_path

    @stdout_log_path.setter
    def stdout_log_path(self, stdout_log_path):
        if stdout_log_path is not None and \
                not isinstance(stdout_log_path, str):
            raise TypeError(
                'stdout log path must be a str: %r' % (stdout_log_path)
            )
        self._stdout_log_path = stdout_log_path

    @property
    def stderr_log_path(self):
        return self._stderr_log_path

    @stderr_log_path.setter
    def stderr_log_path(self, stderr_log_path):
        if stderr_log_path is not None and \
                not isinstance(stderr_log_path, str):
            raise TypeError(
                'stderr_log_path must be a str: %r' % (stderr_log_path)
            )
        self._stderr_log_path = stderr_log_path

    @property
    def keep_logs(self):
        if self._keep_logs is None:
            return False
        return self._keep_logs

    @keep_logs.setter
    def keep_logs(self, keep_logs):
        if keep_logs is not None and not isinstance(keep_logs, bool):
            raise TypeError('keep-logs must be a bool: %r' % (keep_logs))
        self._keep_logs = keep_logs

    @property
    def ycmd_module_directory(self):
        if self._ycmd_root_directory is None:
            logger.error('no ycmd root directory set')
            raise AttributeError
        return os.path.join(self._ycmd_root_directory, 'ycmd')

    def copy(self):
        '''
        Creates a shallow-copy of the startup parameters.
        '''
        raw_attrs = [
            '_ycmd_root_directory',
            '_ycmd_settings_path',
            '_working_directory',
            '_python_binary_path',
            '_server_idle_suicide_seconds',
            '_server_check_interval_seconds',
            '_log_level',
            '_stdout_log_path',
            '_stderr_log_path',
            '_keep_logs',
        ]
        result = StartupParameters()

        for attr in raw_attrs:
            attr_value = getattr(self, attr)
            setattr(result, attr, attr_value)

        return result

    def __iter__(self):
        ''' Dictionary-compatible iterator. '''
        return iter((
            ('ycmd_root_directory', self.ycmd_root_directory),
            ('ycmd_settings_path', self.ycmd_settings_path),
            ('working_directory', self.working_directory),
            ('python_binary_path', self.python_binary_path),
            ('server_idle_suicide_seconds', self.server_idle_suicide_seconds),
            (
                'server_check_interval_seconds',
                self.server_check_interval_seconds,
            ),
            ('ycmd_module_directory', self.ycmd_module_directory),
            ('log_level', self.log_level),
            ('stdout_log_path', self.stdout_log_path),
            ('stderr_log_path', self.stderr_log_path),
            ('keep_logs', self.keep_logs),
        ))

    def __str__(self):
        return (
            'ycmd path, default settings path, '
            'python binary path, working directory: '
            '%(ycmd_root_directory)s, %(ycmd_settings_path)s, '
            '%(python_binary_path)s, %(working_directory)s' %
            (dict(self))
        )

    def __repr__(self):
        return '%s(%r)' % (StartupParameters, dict(self)) def to_startup_parameters(ycmd_root_directory,
                          ycmd_settings_path=None,
                          working_directory=None,
                          python_binary_path=None,
                          server_idle_suicide_seconds=None,
                          server_check_interval_seconds=None):
    '''
    Internal convenience function. Receives the raw arguments to starting a
    ycmd server and returns a `StartupParameters` instance from it.

    If the first argument is already `StartupParameters`, it is returned as-is,
    and the remaining parameters are ignored.

    Otherwise, a `StartupParameters` instance is constructed with all the given
    parameters and returned.
    '''
    if isinstance(ycmd_root_directory, StartupParameters):
        # great, already in the desired state
        # check if other params are provided and issue a warning
        # (they get ignored in that case)
        if ycmd_settings_path is not None:
            logger.warning(
                'ycmd settings path will be ignored: %s', ycmd_settings_path,
            )
        if working_directory is not None:
            logger.warning(
                'working directory will be ignored: %s', working_directory,
            )
        if python_binary_path is not None:
            logger.warning(
                'python binary path will be ignored: %s', python_binary_path,
            )
        if server_idle_suicide_seconds is not None:
            logger.warning(
                'server idle suicide seconds will be ignored: %s',
                server_idle_suicide_seconds,
            )
        if server_check_interval_seconds is not None:
            logger.warning(
                'server check interval seconds will be ignored: %s',
                server_check_interval_seconds,
            )

        return ycmd_root_directory

    # else, generate them
    logger.warning('[DEPRECATED] to startup parameters', stack_info=True)
    logger.debug(
        'generating startup parameters with root: %s', ycmd_root_directory,
    )

    return StartupParameters(
        ycmd_root_directory,
        ycmd_settings_path=ycmd_settings_path,
        working_directory=working_directory,
        python_binary_path=python_binary_path,
        server_idle_suicide_seconds=server_idle_suicide_seconds,
        server_check_interval_seconds=server_check_interval_seconds,
    ) def write_ycmd_settings_file(ycmd_settings_path, ycmd_hmac_secret, out=None):
    '''
    Writes out a ycmd server settings file based on the template file
    `ycmd_settings_path`. A uniquely-generated `ycmd_hmac_secret` must also be
    supplied, as it needs to be written into this file.
    The return value is the path to the settings file, as a `str`.
    If `out` is omitted, a secure temporary file is created, and the returned
    path should be passed via the options flag to ycmd.
    If `out` is provided, it should be a path to an output file (`str`), or a
    file-like handle (must support `.write`). This is not recommended for use
    with ycmd, as it may be insecure.
    '''
    ycmd_settings_data = generate_settings_data(
        ycmd_settings_path, ycmd_hmac_secret,
    )

    out_path = None

    if out is None:
        # no point using `with` for this, since we also use `delete=False`
        temp_file_object = tempfile.NamedTemporaryFile(
            prefix='ycmd_settings_', suffix='.json', delete=False,
        )
        temp_file_name = temp_file_object.name
        temp_file_handle = temp_file_object.file    # type: io.TextIOWrapper

        out = temp_file_handle
        out_path = temp_file_name

        def flush():
            temp_file_handle.flush()

        def close():
            temp_file_object.close()
    else:
        raise NotImplementedError('unimplemented: output to specific file')

    if out_path is None and out is not None:
        logger.error('failed to get path for output file: %r', out)
        # fall through and write it out anyway

    save_json_file(out, ycmd_settings_data)

    flush()
    close()

    logger.debug('successfully wrote file: %s', out_path)
    return out_path def prepare_ycmd_process(startup_parameters, ycmd_settings_tempfile_path,
                         ycmd_server_hostname, ycmd_server_port):
    '''
    Initializes and returns a `Process` handle, correctly configured to launch
    a ycmd server process. It does not automatically start it though.
    The `ycmd_settings_tempfile_path` should be created by (return value of)
    `write_ycmd_settings_file`. The ycmd server process will read that file on
    startup and then immediately delete it.
    The `ycmd_server_hostname` and `ycmd_server_port` must also be provided to
    instruct the server to listen on the given address.
    '''
    assert isinstance(startup_parameters, StartupParameters), \
        'startup parameters must be StartupParameters: %r' % \
        (startup_parameters)
    assert isinstance(ycmd_settings_tempfile_path, str), \
        'ycmd settings temporary file path must be a str: %r' % \
        (ycmd_settings_tempfile_path)

    # this may throw:
    check_startup_parameters(startup_parameters)

    working_directory = startup_parameters.working_directory
    python_binary_path = startup_parameters.python_binary_path
    server_idle_suicide_seconds = \
        startup_parameters.server_idle_suicide_seconds
    server_check_interval_seconds = \
        startup_parameters.server_check_interval_seconds
    ycmd_module_directory = startup_parameters.ycmd_module_directory

    if YCMD_LOG_SPOOL_OUTPUT:
        stdout_log_spool = \
            tempfile.SpooledTemporaryFile(max_size=YCMD_LOG_SPOOL_SIZE)
        stderr_log_spool = \
            tempfile.SpooledTemporaryFile(max_size=YCMD_LOG_SPOOL_SIZE)

        logger.debug(
            'using temporary spools for stdout, stderr: %r, %r',
            stdout_log_spool, stderr_log_spool,
        )

        stdout_handle = stdout_log_spool
        stderr_handle = stderr_log_spool
    else:
        # explicitly close handles - don't inherit from this process
        stdout_handle = FileHandles.DEVNULL
        stderr_handle = FileHandles.DEVNULL

    ycmd_process_handle = Process()

    ycmd_process_handle.binary = python_binary_path
    ycmd_process_handle.args.extend([
        ycmd_module_directory,
        '--host=%s' % (ycmd_server_hostname),
        '--port=%s' % (ycmd_server_port),
        '--idle_suicide_seconds=%s' % (server_idle_suicide_seconds),
        '--check_interval_seconds=%s' % (server_check_interval_seconds),
        '--options_file=%s' % (ycmd_settings_tempfile_path),
    ])

    ycmd_process_handle.cwd = working_directory
    ycmd_process_handle.filehandles.stdout = stdout_handle
    ycmd_process_handle.filehandles.stderr = stderr_handle

    if startup_parameters.log_level is not None:
        add_ycmd_debug_args(
            ycmd_process_handle,
            log_level=startup_parameters.log_level,
            stdout_file_name=startup_parameters.stdout_log_path,
            stderr_file_name=startup_parameters.stderr_log_path,
            keep_logfiles=startup_parameters.keep_logs,
        )

    return ycmd_process_handle
import http
import logging
import os
import threading
from ..process import Process
from ..schema.completions import parse_completions
from ..schema.request import RequestParameters
from ..util.format import (
    json_serialize,
    json_parse,
)
from ..util.fs import (
    is_file,
    get_base_name,
)
from ..util.hmac import (
    calculate_hmac,
    new_hmac_secret,
)
from ..util.lock import lock_guard
from ..util.str import (
    str_to_bytes,
    truncate,
)
from ..util.sys import get_unused_port
from ..ycmd.constants import (
    YCMD_EVENT_BUFFER_UNLOAD,
    YCMD_EVENT_BUFFER_VISIT,
    YCMD_EVENT_CURRENT_IDENTIFIER_FINISHED,
    YCMD_EVENT_FILE_READY_TO_PARSE,
    YCMD_EVENT_INSERT_LEAVE,
    YCMD_HANDLER_DEBUG_INFO,
    YCMD_HANDLER_DEFINED_SUBCOMMANDS,
    YCMD_HANDLER_EVENT_NOTIFICATION,
    YCMD_HANDLER_GET_COMPLETIONS,
    YCMD_HANDLER_HEALTHY,
    YCMD_HANDLER_IGNORE_EXTRA_CONF,
    YCMD_HANDLER_LOAD_EXTRA_CONF,
    YCMD_HANDLER_SHUTDOWN,
    YCMD_HMAC_HEADER,
    YCMD_HMAC_SECRET_LENGTH,
)
from ..ycmd.start import (
    StartupParameters,
    to_startup_parameters,
    write_ycmd_settings_file,
    prepare_ycmd_process,
)
        headers = {}
        if hmac_headers:
            headers.update(hmac_headers)
        if content_type_headers:
            headers.update(content_type_headers)

        self._logger.debug(
            'about to send a request with '
            'method, handler, params, headers: %s, %s, %s, %s',
            method, handler, truncate(json_params), truncate(headers),
        )

        with self._lock:
            host = self.hostname
            port = self.port

        response_status = None
        response_reason = None
        response_headers = None
        response_data = None
        try:
            connection = http.client.HTTPConnection(
                host=host, port=port, timeout=timeout,
            )
            connection.request(
                method=method,
                url=handler,
                body=body,
                headers=headers,
            )

            response = connection.getresponse()

            response_status = response.status
            response_reason = response.reason

            # verify response hmac before using it
            response_headers = response.getheaders()
            response_content_type = response.getheader('Content-Type')
            response_content_length = response.getheader('Content-Length', 0)
            response_content_hmac = response.getheader(YCMD_HMAC_HEADER)

            try:
                response_content_length = int(response_content_length)
            except ValueError:
                response_content_length = 0

            has_content = response_content_length > 0
            is_content_json = response_content_type == 'application/json'

            # extract response to check hmac
            if has_content:
                response_content = response.read()
                with self._lock:
                    hmac = self._hmac

                expected_content_hmac = calculate_hmac(
                    hmac, response_content,
                )

                if response_content_hmac != expected_content_hmac:
                    self._logger.error(
                        'server responded with incorrect hmac, '
                        'dropping response - expected, received: %r, %r',
                        expected_content_hmac, response_content_hmac,
                    )
                    response_content = None
                else:
                    self._logger.debug(
                        'response hmac matches, response is valid',
                    )
            else:
                response_content = None

            # parse response as json
            if response_content and is_content_json:
                response_data = json_parse(response_content)
            else:
                if has_content or response_content:
                    self._logger.warning(
                        'ycmd server response is not json, content type: %r',
                        response_content_type,
                    )
                response_data = None

        except http.client.HTTPException as e:
            self._logger.error('error during ycmd request: %s', e, exc_info=e)
        except ConnectionError as e:
            self._logger.debug(
                'connection error, ycmd server may be dead: %s', e,
            )

        self._logger.debug(
            'parsed status, reason, headers, data: %s, %s, %s, %s',
            response_status, response_reason, response_headers, response_data,
        )

        return response_data

    def get_completer_commands(self, request_params):
        return self._send_request(
            YCMD_HANDLER_DEFINED_SUBCOMMANDS,
            request_params=request_params,
            method='POST',
        )

    def get_debug_info(self, request_params, timeout=None):
        return self._send_request(
            YCMD_HANDLER_DEBUG_INFO,
            request_params=request_params,
            method='POST',
            timeout=timeout,
        )

    def get_code_completions(self, request_params, timeout=None):
        assert isinstance(request_params, RequestParameters), \
            'request parameters must be RequestParameters: %r' % \
            (request_params)

        completion_data = self._send_request(

            YCMD_HANDLER_GET_COMPLETIONS,