Skip to content

Commit

Permalink
Cleaned up and refactored. Split apart large modules.
Browse files Browse the repository at this point in the history
Modified the logging system to hook into a formatter, instead of trying to
modify records with a filter. This makes it a lot less fragile.

Split the server logic into multiple smaller modules.

Split the sublime-utility logic into multiple smaller modules.

Added a dedicated schema module to define the json structures.
  • Loading branch information
akshayganeshen committed Sep 19, 2017
1 parent eb0bac2 commit 5dbffa3
Show file tree
Hide file tree
Showing 30 changed files with 2,161 additions and 1,775 deletions.
20 changes: 20 additions & 0 deletions .coveragerc
@@ -0,0 +1,20 @@
[run]
branch = True
source =
lib

[paths]
source =
lib/

[report]
exclude_lines =
pragma: no cover
def __repr__
raise NotImplementedError

omit =
*/__init__.py
*/tests/*

ignore_errors = True
11 changes: 11 additions & 0 deletions lib/process/__init__.py
@@ -0,0 +1,11 @@
#!/usr/bin/env python3

'''
lib/process
sublime-ycmd process management module.
'''

from lib.process.process import Process # noqa
from lib.process.filehandles import FileHandles # noqa

__all__ = ['process', 'filehandles']
99 changes: 99 additions & 0 deletions lib/process/filehandles.py
@@ -0,0 +1,99 @@
#!/usr/bin/env python3

'''
lib/process/process.py
Process file handle wrapper class.
Provides a way to customize the stdin, stdout, and stderr file handles of a
process before launching it. By default, all handles will be closed, which
means it would be impossible to write data to it or read output from it.
'''

import io
import logging
import subprocess

logger = logging.getLogger('sublime-ycmd.' + __name__)


class FileHandles(object):
'''
Container class for process file handles (stdin, stdout, stderr).
Provides an option to set up PIPEs when starting processes, and then allows
reading/writing to those pipes with helper methods.
'''

PIPE = subprocess.PIPE
DEVNULL = subprocess.DEVNULL
STDOUT = subprocess.STDOUT

def __init__(self):
self._stdin = None
self._stdout = None
self._stderr = None

def configure_filehandles(self, stdin=None, stdout=None, stderr=None):
'''
Sets the file handle behaviour for the launched process. Each handle
can be assigned one of the subprocess handle constants:
stdin = None, PIPE
stdout = None, PIPE, DEVNULL
stderr = None, PIPE, DEVNULL, STDOUT
These value gets forwarded directly to Popen.
'''
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr

@staticmethod
def valid_filehandle(handle):
''' Validates that a handle is `None`, or a file-like object. '''
if handle is None or isinstance(handle, io.IOBase):
return True

# explicitly allow the special flags as well
return handle in [
FileHandles.PIPE,
FileHandles.DEVNULL,
FileHandles.STDOUT,
]

@property
def stdin(self):
''' Returns the configured stdin handle. '''
return self._stdin

@stdin.setter
def stdin(self, stdin):
''' Sets the stdin file handle. '''
if not self.valid_filehandle(stdin):
raise ValueError('stdin handle must be a file instance')
if stdin == self.STDOUT:
# what?
raise ValueError('stdin handle cannot be redirected to stdout')
self._stdin = stdin

@property
def stdout(self):
''' Returns the configured stdout handle. '''
return self._stdout

@stdout.setter
def stdout(self, stdout):
if not self.valid_filehandle(stdout):
raise ValueError('stdout handle must be a file instance')
# do not allow STDOUT
if stdout == self.STDOUT:
raise ValueError('stdout handle cannot be redirected to stdout')
self._stdout = stdout

@property
def stderr(self):
''' Returns the configured stderr handle. '''
return self._stderr

@stderr.setter
def stderr(self, stderr):
if not self.valid_filehandle(stderr):
raise ValueError('stderr handle must be a file instance')
self._stderr = stderr
123 changes: 23 additions & 100 deletions lib/process.py → lib/process/process.py
@@ -1,106 +1,27 @@
#!/usr/bin/env python3

'''
lib/process.py
Contains utilities for working with processes. This is used to start and manage
the ycmd server process.
lib/process/process.py
Process wrapper class.
Provides utilities for managing processes. This is useful for starting the ycmd
server process, checking if it's still alive, and shutting it down.
'''

import io
import logging
import os
import subprocess

from lib.fs import (
from lib.process.filehandles import FileHandles
from lib.util.fs import (
is_directory,
is_file,
resolve_binary_path,
)

logger = logging.getLogger('sublime-ycmd.' + __name__)


class SYfileHandles(object):
'''
Container class for process file handles (stdin, stdout, stderr).
Provides an option to set up PIPEs when starting processes, and then allows
reading/writing to those pipes with helper methods.
'''

PIPE = subprocess.PIPE
DEVNULL = subprocess.DEVNULL
STDOUT = subprocess.STDOUT

def __init__(self):
self._stdin = None
self._stdout = None
self._stderr = None

def configure_filehandles(self, stdin=None, stdout=None, stderr=None):
'''
Sets the file handle behaviour for the launched process. Each handle
can be assigned one of the subprocess handle constants:
stdin = None, PIPE
stdout = None, PIPE, DEVNULL
stderr = None, PIPE, DEVNULL, STDOUT
These value gets forwarded directly to Popen.
'''
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr

@staticmethod
def valid_filehandle(handle):
''' Validates that a handle is None, or a file-like object. '''
if handle is None or isinstance(handle, io.IOBase):
return True
# explicitly allow the special flags as well
if handle in [SYfileHandles.PIPE,
SYfileHandles.DEVNULL,
SYfileHandles.STDOUT]:
return True
return False

@property
def stdin(self):
''' Returns the configured stdin handle. '''
return self._stdin

@stdin.setter
def stdin(self, stdin):
''' Sets the stdin file handle. '''
if not self.valid_filehandle(stdin):
raise ValueError('stdin handle must be a file instance')
if stdin == self.STDOUT:
# what?
raise ValueError('stdin handle cannot be redirected to stdout')
self._stdin = stdin

@property
def stdout(self):
''' Returns the configured stdout handle. '''
return self._stdout

@stdout.setter
def stdout(self, stdout):
if not self.valid_filehandle(stdout):
raise ValueError('stdout handle must be a file instance')
# do not allow STDOUT
if stdout == self.STDOUT:
raise ValueError('stdout handle cannot be redirected to stdout')
self._stdout = stdout

@property
def stderr(self):
''' Returns the configured stderr handle. '''
return self._stderr

@stderr.setter
def stderr(self, stderr):
if not self.valid_filehandle(stderr):
raise ValueError('stderr handle must be a file instance')
self._stderr = stderr


class SYprocess(object):
class Process(object):
'''
Process class
This class represents a managed process.
Expand All @@ -111,7 +32,7 @@ def __init__(self):
self._args = None
self._env = None
self._cwd = None
self._filehandles = SYfileHandles()
self._filehandles = FileHandles()

self._handle = None

Expand All @@ -125,7 +46,7 @@ def binary(self, binary):
''' Sets the process binary. '''
assert isinstance(binary, str), 'binary must be a string: %r' % binary
binary = resolve_binary_path(binary)
assert os.path.isfile(binary), 'binary path invalid: %r' % binary
assert is_file(binary), 'binary path invalid: %r' % binary

logger.debug('setting binary to: %s', binary)
self._binary = binary
Expand All @@ -144,6 +65,8 @@ def args(self, args):
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)
Expand All @@ -153,7 +76,7 @@ def args(self, args):

@property
def env(self):
''' Returns the process env. Initializes it if it is None. '''
''' Returns the process env. Initializes it if it is `None`. '''
if self._env is None:
self._env = {}
return self._env
Expand Down Expand Up @@ -184,7 +107,7 @@ def cwd(self, cwd):
logger.warning('process already started... no point setting cwd')

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

logger.debug('setting cwd to: %s', cwd)
Expand Down Expand Up @@ -235,15 +158,15 @@ 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 input is None, stdin is immediately closed.
When timeout is None, this waits indefinitely for the process to
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.
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
assert self._handle is not None, '[internal] process handle is null'
return self._handle.communicate(inpt, timeout)

def wait(self, maxwait=10):
Expand All @@ -252,14 +175,14 @@ def wait(self, maxwait=10):
logger.debug('process not alive, nothing to wait for')
return

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

def kill(self):
''' Kills the associated process, by sending a signal. '''
''' 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
assert self._handle is not None, '[internal] process handle is null'
self._handle.kill()
12 changes: 12 additions & 0 deletions lib/schema/__init__.py
@@ -0,0 +1,12 @@
#!/usr/bin/env python3

'''
lib/schema
sublime-ycmd json schema definitions.
'''

from lib.schema.request import RequestParameters # noqa
from lib.schema.completions import ( # noqa
Completions,
CompletionOption,
)

0 comments on commit 5dbffa3

Please sign in to comment.