def auto_build(self): current_project_dir, current_project_name = Common.locate_cabal_project_from_view(self.view) if current_project_name and current_project_dir: build_mode = Settings.get_project_setting(self.view.window().active_view(), 'auto_build_mode', Settings.PLUGIN.auto_build_mode) build_command = self.MODE_BUILD_COMMAND.get(build_mode) if not build_command: Common.output_error(self.view.window(), "SublimeHaskell: invalid auto_build_mode '%s'" % build_mode) return # Duplicate the dictionary corresponding to the build command. We might modify it later. config = dict(self.BUILD_TOOL_CONFIG[build_command]) addl_config = None if build_mode.endswith('-then-tests'): has_tests = False projects = self.get_projects() if current_project_name in projects and 'description' in projects[current_project_name]: has_tests = projects[current_project_name]['description'].get('tests') is not None if has_tests: addl_config = 'test' elif build_mode.endswith('-then-bench'): addl_config = 'bench' if addl_config is not None: for tool, steps in self.BUILD_TOOL_CONFIG[addl_config]['steps'].items(): config['steps'][tool].extend(steps) Logging.log('auto build: final config:\n{0}'.format(pprint.pformat(config))) self.run_build(current_project_name, current_project_dir, config)
def select_project(window, on_selected, filter_project=None): projs = [(name, info) for (name, info) in get_projects().items() if not filter_project or filter_project(name, info)] def run_selected(psel): on_selected(psel[0], psel[1]['path']) if len(projs) == 0: Common.show_status_message( "No projects found, did you add a '.cabal' file?", is_ok=False, priority=5) return if len(projs) == 1: # There's only one project, build it run_selected(projs[0]) return _, cabal_project_name = Common.get_cabal_project_dir_and_name_of_view( window.active_view()) Logging.log('Current project: {0}'.format(cabal_project_name)) # Sort by name projs.sort(key=lambda p: p[0]) current_project_idx = next( (i for i, p in enumerate(projs) if p[0] == cabal_project_name), -1) def on_done(idx): if idx != -1: run_selected(projs[idx]) window.show_quick_panel(list(map(lambda m: [m[0], m[1]['path']], projs)), on_done, 0, current_project_idx)
def connect(self, host, port): for i in range(0, self.N_SOCKETS): sock = self.create_connection(i, host, port) if sock is not None: hsconn = HsDevConnection(sock, self.request_q, i) self.socket_pool.insert(i, hsconn) else: # Something went wrong, terminate all connnections: for sock in self.socket_pool: try: sock.close() except OSError: pass self.socket_pool = [] self.request_q = None return False # We were successful, start all of the socket threads... for sock in self.socket_pool: sock.start_connection() self.rcvr_thread = threading.Thread(target=self.receiver) self.rcvr_thread.start() Logging.log('Connections established to \'hsdev\' server.', Logging.LOG_INFO) return True
def go_chain(self, cmds): try: if not cmds: self.status_msg.stop() hsdev.client.autofix_show(self.msgs, on_response=self.on_autofix) else: cmd, tail_cmds = cmds[0], cmds[1:] (chain_fn, modify_args, modify_msgs, kwargs) = cmd def on_resp(msgs): self.messages.extend(modify_msgs(msgs)) self.msgs.extend(msgs) self.go_chain(tail_cmds) def on_err(_err, _details): self.status_msg.fail() self.go_chain([]) chain_fn(modify_args(self.filename), contents=self.contents, wait=False, on_response=on_resp, on_error=on_err, **kwargs) except OSError: Logging.log( 'hsdev chain failed, see console window (<ctrl>-<backtick>) traceback', Logging.LOG_ERROR) print(traceback.format_exc()) self.status_msg.fail() self.status_msg.stop()
def exec_with_wrapper(exec_with, install_dir, cmd_list): '''Wrapper function for inserting the execution wrapper, e.g., 'cabal exec' or 'stack exec' :returns: Process object from ProcHelper. ''' proc_args = {} if exec_with is not None: if exec_with == 'cabal': cmd_list = ['cabal', 'exec'] + cmd_list cmd_list.insert(3, '--') elif exec_with == 'stack': cmd_list = ['stack', 'exec'] + cmd_list cmd_list.insert(3, '--') else: errmsg = 'HsDevBackend.exec_with_wrapper: Unknown execution prefix \'{0}\''.format( exec_with) raise RuntimeError(errmsg) if install_dir is not None: proc_args['cwd'] = Utils.normalize_path(install_dir) else: cmd = Which.which(cmd_list[0], ProcHelper.ProcHelper.get_extended_path()) if cmd is not None: cmd_list[0] = cmd Logging.log('HsDevBackend.exec_with_wrapper: {0}'.format(cmd_list), Logging.LOG_DEBUG) return ProcHelper.ProcHelper(cmd_list, **proc_args)
def select_project(self, on_selected, filter_project): projs = [(name, info) for (name, info) in self.get_projects().items() if not filter_project or filter_project(name, info)] def run_selected(psel): on_selected(psel[0], psel[1]['path']) if not projs: Common.show_status_message("No active projects found.", is_ok=False, priority=5) elif len(projs) == 1: # There's only one project, build it run_selected(projs[0]) else: _, cabal_project_name = Common.locate_cabal_project_from_view( self.window.active_view()) Logging.log('Current project: {0}'.format(cabal_project_name)) # Sort by name projs.sort(key=lambda p: p[0]) current_project_idx = next( (i for i, p in enumerate(projs) if p[0] == cabal_project_name), -1) def on_done(idx): if idx != -1: run_selected(projs[idx]) self.window.show_quick_panel( list(map(lambda m: [m[0], m[1]['path']], projs)), on_done, 0, current_project_idx)
def select_project(self, on_selected, filter_project): '''Select a project from a generated project list. Execution flow continues into the :py:function:`on_selected` function with the project's name and the project's base directory. The :py:function:`filter_project` filters projects before they are shown (see :py:method:`get_projects`.) ''' projs = [(name, info) for (name, info) in self.get_projects().items() if not filter_project or filter_project(name, info)] def run_selected(psel): on_selected(psel[0], psel[1]['path']) if not projs: Common.sublime_status_message("No active projects found.") elif len(projs) == 1: # There's only one project, build it run_selected(projs[0]) else: _, cabal_project_name = Common.locate_cabal_project_from_view(self.view) Logging.log('Current project: {0}'.format(cabal_project_name)) # Sort by name projs.sort(key=lambda p: p[0]) current_project_idx = next((i for i, p in enumerate(projs) if p[0] == cabal_project_name), -1) def on_done(idx): if idx != -1: run_selected(projs[idx]) self.view.window().show_quick_panel([[m[0], m[1].get('path', '??')] for m in projs], on_done, 0, current_project_idx)
def try_decode_bytes(src): try: return decode_bytes(src) except Exception as e: Logging.log('Error decoding bytes from subprocess: {}'.format(src), Logging.LOG_ERROR) return '\n'
def invoke_tool(command, tool_name, inp='', on_result=None, filename=None, on_line=None, check_enabled=True, **popen_kwargs): if check_enabled and not Settings.PLUGIN.__getattribute__(Utils.tool_enabled(tool_name)): return None source_dir = get_source_dir(filename) def mk_result(result): return on_result(result) if on_result else result try: with ProcHelper(command, cwd=source_dir, **popen_kwargs) as proc: exit_code, stdout, stderr = proc.wait(inp) if exit_code != 0: raise Exception('{0} exited with exit code {1} and stderr: {2}'.format(tool_name, exit_code, stderr)) if on_line: for line in io.StringIO(stdout): on_line(mk_result(line)) else: return mk_result(stdout) except OSError as os_exc: if os_exc.errno == errno.ENOENT: errmsg = "SublimeHaskell: {0} was not found!\n'{1}' is set to False".format(tool_name, Utils.tool_enabled(tool_name)) Common.output_error_async(sublime.active_window(), errmsg) Settings.PLUGIN.__setattr__(Utils.tool_enabled(tool_name), False) else: Logging.log('{0} fails with {1}, command: {2}'.format(tool_name, os_exc, command), Logging.LOG_ERROR) return None return None
def auto_build(self): current_project_dir, current_project_name = Common.locate_cabal_project_from_view(self.view) if current_project_name and current_project_dir: build_mode = Settings.get_project_setting(self.view.window().active_view(), 'auto_build_mode', Settings.PLUGIN.auto_build_mode) build_command = self.MODE_BUILD_COMMAND.get(build_mode) if not build_command: Common.output_error(self.view.window(), "SublimeHaskell: invalid auto_build_mode '%s'" % build_mode) return # Duplicate the dictionary corresponding to the build command. We might modify it later. config = dict(self.BUILD_TOOL_CONFIG[build_command]) addl_config = None if build_mode.endswith('-then-tests'): has_tests = False projects = self.get_projects() if current_project_name in projects and 'description' in projects[current_project_name]: has_tests = projects[current_project_name]['description'].get('tests') is not None if has_tests: addl_config = 'test' elif build_mode.endswith('-then-bench'): addl_config = 'bench' if addl_config is not None: for tool, steps in self.BUILD_TOOL_CONFIG[addl_config]['steps'].items(): config['steps'][tool].extend(steps) Logging.log('auto build: final config:\n{0}'.format(pprint.pformat(config))) self.run_build(current_project_name, current_project_dir, config)
def read_response(self): resp_stdout = [] resp_stderr = [] try: got_reply = False while not got_reply: resp = self.ghcmod.process.stdout.readline() if resp == '': # EOF??? got_reply = True else: prefix = resp[0:3] resp = resp.rstrip()[3:] if prefix == self.GHCMOD_OUTPUT_MARKER: if resp == 'OK': got_reply = True else: resp_stdout.append(resp.rstrip()) elif prefix == self.GHCMOD_ERROR_MARKER: Logging.log('{0}: {1}'.format(self.diag_prefix, resp)) resp_stderr.append(resp) elif prefix == 'NG ': sys.stdout.write('Error response: ' + resp) got_reply = True else: sys.stdout.write( 'Unexpected reply from ghc-mod client: ' + resp) got_reply = True except OSError: self.shutdown() return (resp_stdout, resp_stderr)
def run_chain(self, cmds, msg, fly_mode=False): self.messages = [] self.msgs = [] self.corrections = [] self.fly_mode = fly_mode self.filename = self.view.file_name() if not self.filename: return self.contents = {} if self.view.is_dirty(): self.contents[self.filename] = self.view.substr( sublime.Region(0, self.view.size())) if not self.fly_mode: ParseOutput.hide_output(self.view) if cmds: self.status_msg = Common.status_message_process(msg + ': ' + self.filename, priority=2) self.status_msg.start() if not hsdev.agent_connected(): Logging.log('hsdev chain fails: hsdev not connected', Logging.LOG_ERROR) self.status_msg.fail() self.status_msg.stop() else: self.go_chain(cmds)
def initialize(self): if self.current_state(self.INITIAL): with self.action_lock: # Can only start a backend iff in the INITIAL state. Logging.log( 'Starting backend \'{0}\''.format( self.current_backend_name), Logging.LOG_INFO) backend_info = self.possible_backends.get( self.current_backend_name, {}) backend_clazz = self.BACKEND_META.get( backend_info.get('backend') or Backend.NullHaskellBackend.backend_name()) if backend_clazz is not None: the_backend = backend_clazz( self, **backend_info.get('options', {})) else: the_backend = None if the_backend is not None: self.state_startup(the_backend) self.state_connect(the_backend) self.state_active(the_backend) if self.current_state(self.INITIAL): # Something failed during startup, revert to the null backend self.set_backend(Backend.NullHaskellBackend(self)) elif not self.current_state(self.ACTIVE): state_str = self.STATES_TO_NAME.get( self.state, str(self.state)) Logging.log( 'BackendManager: Invalid state after starting backend: {0}' .format(state_str), Logging.LOG_ERROR)
def select_project(self, on_selected, filter_project): '''Select a project from a generated project list. Execution flow continues into the :py:function:`on_selected` function with the project's name and the project's base directory. The :py:function:`filter_project` filters projects before they are shown (see :py:method:`get_projects`.) ''' projs = [(name, info) for (name, info) in self.get_projects().items() if not filter_project or filter_project(name, info)] def run_selected(psel): on_selected(psel[0], psel[1]['path']) if not projs: Common.sublime_status_message("No active projects found.") elif len(projs) == 1: # There's only one project, build it run_selected(projs[0]) else: _, cabal_project_name = Common.locate_cabal_project_from_view(self.view) Logging.log('Current project: {0}'.format(cabal_project_name)) # Sort by name projs.sort(key=lambda p: p[0]) current_project_idx = next((i for i, p in enumerate(projs) if p[0] == cabal_project_name), -1) def on_done(idx): if idx != -1: run_selected(projs[idx]) self.view.window().show_quick_panel([[m[0], m[1].get('path', '??')] for m in projs], on_done, 0, current_project_idx)
def run_chain(self, cmds, msg, fly_mode=False): self.messages = [] self.msgs = [] self.corrections = [] self.fly_mode = fly_mode self.filename = self.view.file_name() if self.filename: self.contents = {} if self.view.is_dirty(): self.contents[self.filename] = self.view.substr( sublime.Region(0, self.view.size())) if not self.fly_mode: ParseOutput.hide_output(self.view) if not hsdev.agent_connected(): Logging.log('hsdev chain fails: hsdev not connected', Logging.LOG_ERROR) sublime.error_message( 'check_lint.run_chain: Cannot execute command chain, hsdev not connected.' ) else: if cmds: self.status_msg = Common.status_message_process( msg + ': ' + self.filename, priority=2) self.status_msg.start() self.go_chain(cmds) else: sublime.error_message( 'Empty command chain (check_lint.run_chain)')
def go_chain(self, cmds): if not cmds: self.status_msg.stop() hsdev.client.autofix_show(self.msgs, on_response=self.on_autofix) else: cmd, tail_cmds = cmds[0], cmds[1:] agent_func, modify_args, modify_msgs, kwargs = cmd def go_chain_resp(msgs): Logging.log('go_chain_resp:\n{0}'.format(pprint.pformat(msgs)), Logging.LOG_DEBUG) self.messages.extend(modify_msgs(msgs)) self.msgs.extend(msgs) self.go_chain(tail_cmds) def go_chain_err(_err, details): Logging.log( 'go_chain_err: details\n{0}'.format( pprint.pformat(details)), Logging.LOG_DEBUG) self.status_msg.fail() self.go_chain([]) Logging.log('go_chain: executing\n{0}'.format(pprint.pformat(cmd)), Logging.LOG_DEBUG) agent_func(modify_args(self.filename), contents=self.contents, wait=False, on_response=go_chain_resp, on_error=go_chain_err, **kwargs)
def drop_completions_async(self, file_name=None): Logging.log('drop prepared completions', Logging.LOG_DEBUG) with self.cache as cache_: if file_name is None: cache_.files.clear() elif file_name in cache_.files: del cache_.files[file_name]
def make_augmented_path(): ''' Generate the augmented PATH for subprocesses: adds the appropriate cabal/stack local install directory ($HOME/.local/bin for *nix, %APPDATA%/local/bin for Windows) and updates PATH with `add_to_PATH` extras. ''' std_places = [] if Settings.PLUGIN.add_standard_dirs: std_places.append("$HOME/.local/bin" if not Utils.is_windows() else "%APPDATA%/local/bin") if Utils.is_macosx(): std_places.append('$HOME/Library/Haskell/bin') std_places += CabalConfigRdr.cabal_config() std_places = [ dir for dir in [Utils.normalize_path(path) for path in std_places] if os.path.isdir(dir) ] add_to_path = list( filter(os.path.isdir, map(Utils.normalize_path, Settings.PLUGIN.add_to_path))) Logging.log("std_places = {0}".format(std_places), Logging.LOG_INFO) Logging.log("add_to_PATH = {0}".format(add_to_path), Logging.LOG_INFO) return os.pathsep.join(add_to_path + std_places)
def run(self, edit, **kwargs): current_file_name = kwargs.get('filename', self.view.file_name()) project_name = Common.locate_cabal_project_from_view(self.view)[1] backend = BackendManager.active_backend() imp_module = Utils.head_of(backend.module(project_name, file=current_file_name)) if imp_module: imports = sorted(imp_module.imports, key=lambda i: i.position.line) supported, result = backend.clean_imports(current_file_name) print(result) if supported: if len(imports) == len(result): Logging.log('replacing imports for {0}'.format(current_file_name), Logging.LOG_TRACE) erased = 0 for imp, new_imp in zip(imports, result): point = self.view.text_point(imp.position.line - 1 - erased, 0) if new_imp.endswith('()'): self.view.erase(edit, self.view.full_line(point)) erased = erased + 1 else: self.view.replace(edit, self.view.line(point), new_imp) else: Common.sublime_status_message('different number of imports: {0} and {1}'.format(len(imports), len(result))) else: if len(result) == 1: Common.sublime_status_message(result[0]) else: sublime.message_dialog('\n'.join(result)) else: Common.sublime_status_message('Clean Imports failed: module not scanned')
def mark_all_files(self): for window in sublime.windows(): with self.dirty_files as dirty_files: dirty_files.update([ (f, None) for f in [v.file_name() for v in window.views()] if f and (f.endswith('.hs') or f.endswith('.hsc')) and f not in dirty_files ]) Logging.log("dirty files: : {0}".format(dirty_files), Logging.LOG_DEBUG)
def lint(self, files=None, contents=None, hlint=None): retval = cmd( 'lint', { 'files': files_and_contents(files or [], contents or {}), 'hlint-opts': hlint or [] }) Logging.log('hsdev.lint: retval\n{0}'.format(pprint.pformat(retval))) return retval
def set_connected(self): if self.is_connecting(): self.connected.set() self.connecting.clear() else: Logging.log( 'HsDev.set_connected called while not in connecting state', Logging.LOG_DEBUG)
def reconnect(self): if self.connect_fun is not None: Logging.log('Reconnecting to hsdev...', Logging.LOG_INFO) HsCallback.call_callback(self.on_reconnect, name='HsDev.on_reconnect') self.connect_fun() else: Logging.log('No reconnect function')
def on_hsdev_enabled(self, key, value): if key == 'enable_hsdev': if value: Logging.log("starting hsdev", Logging.LOG_INFO) self.start_hsdev() else: Logging.log("stopping hsdev", Logging.LOG_INFO) self.stop_hsdev()
def call(self, command, opts, on_response, on_notify, on_error, wait, timeout): # log if not self.verify_connected(): return None if wait else False opts = opts or {} with self.serial_lock: req_serial = str(self.request_serial) self.request_serial = self.request_serial + 1 opts.update({'no-file': True, 'id': req_serial, 'command': command}) wait_receive = threading.Event() if wait else None result_dict = {} def client_call_response(resp): result_dict['result'] = resp HsCallback.call_callback(on_response, resp) if wait_receive is not None: wait_receive.set() def client_call_error(exc, details): HsCallback.call_callback(on_error, exc, details) if wait_receive is not None: wait_receive.set() args_cmd = 'hsdev {0}'.format(command) self.setup_receive_callbacks(req_serial, args_cmd, client_call_response, on_notify, client_call_error, opts) call_cmd = u'HsDevClient.call[{0}] cmd \'{1}\' opts\n{2}'.format(req_serial, command, pprint.pformat(opts)) if Settings.COMPONENT_DEBUG.all_messages or Settings.COMPONENT_DEBUG.send_messages: print(call_cmd) try: # Randomly choose a connection from the pool -- even if we end up having a stuck sender, this should minimize # the probability of a complete hang. random.choice(self.socket_pool).send_request(opts) if wait: wait_receive.wait(timeout) if not wait_receive.is_set(): with self.request_map as requests: req = pprint.pformat(requests[req_serial][1]) errmsg = 'HsDevClient.call: wait_receive event timed out for id {0}\n{1}'.format(req_serial, req) Logging.log(errmsg, Logging.LOG_ERROR) # Delete the request; result_dict will still have nothing in it (presumably) del requests[req_serial] if Settings.COMPONENT_DEBUG.socket_pool: with self.request_map as request_map: print('id {0} request_map {1} queue {2}'.format(req_serial, len(request_map), self.request_q.qsize())) return result_dict.get('result') if wait else True except OSError: self.connection_lost('call', sys.exc_info()[1]) self.backend_mgr.lost_connection() return False
def mark_all_files(self): for window in sublime.windows(): with self.dirty_files as dirty_files: dirty_files.update([(f, None) for f in [v.file_name() for v in window.views()] \ if f and (f.endswith('.hs') or f.endswith('.hsc')) and f not in dirty_files]) Logging.log("dirty files: : {0}".format(dirty_files), Logging.LOG_DEBUG) with self.dirty_paths as dirty_paths: dirty_paths.extend(window.folders())
def run(self): if not Settings.PLUGIN.enable_hsdev: return Settings.PLUGIN.add_change_callback('enable_hsdev', self.on_hsdev_enabled) Settings.PLUGIN.add_change_callback('inspect_modules', self.on_inspect_modules_changed) self.start_hsdev() while True: if Settings.PLUGIN.enable_hsdev and not self.client.ping(): Logging.log('hsdev ping: no pong', Logging.LOG_WARNING) scan_paths = [] with self.dirty_paths as dirty_paths: scan_paths = dirty_paths[:] dirty_paths[:] = [] files_to_reinspect = [] with self.dirty_files as dirty_files: files_to_reinspect = dirty_files[:] dirty_files[:] = [] projects = [] files = [] if len(files_to_reinspect) > 0: projects = [] files = [] for finspect in files_to_reinspect: projdir = Common.get_cabal_project_dir_of_file(finspect) if projdir is not None: projects.append(projdir) else: files.append(finspect) projects = list(set(projects)) files = list(set(files)) self.inspect(paths=scan_paths, projects=projects, files=files) load_cabal = [] with self.cabal_to_load as cabal_to_load: load_cabal = cabal_to_load[:] cabal_to_load[:] = [] for cabal in load_cabal: Worker.run_async('inspect cabal {0}'.format(cabal), self.inspect_cabal, cabal) if files_to_reinspect and Settings.PLUGIN.enable_hdocs: self.client_back.docs(files=files_to_reinspect) self.reinspect_event.wait(HsDevLocalAgent.sleep_timeout) self.reinspect_event.clear()
def worker_run(self): while True: name, worker_fn, args, kwargs = self.jobs.get() try: Logging.log('worker: {0}'.format(name), Logging.LOG_DEBUG) worker_fn(*args, **kwargs) except Exception: Logging.log('worker: job {0} failed, see console window traceback'.format(name), Logging.LOG_ERROR) traceback.print_exc()
def collect_completions(self, backend, modinfo, lookup): if Settings.COMPONENT_DEBUG.completions: print('ghc-mod collect_completions for {0}'.format(modinfo)) modname, is_qualified, qualname = modinfo mod_imported = [symbols.Import(modname, is_qualified, qualname)] syms = backend.command_backend('browse -d -o ' + modname) if backend is not None else [] Logging.log('ghc-mod collect_completions: syms {0}'.format(syms), Logging.LOG_DEBUG) return [self.parse_syminfo(sym, mod_imported) for sym in syms if sym.startswith(lookup)]
def call_error(self, err, details): self.log_time() comb_details = ', '.join( ['{0}: {1}'.format(k, v) for k, v in details.items()]) Logging.log( '{0} returns error: {1}\n{2}'.format(self.command, err, comb_details), Logging.LOG_ERROR) call_callback(self.on_error, err, details)
def connection_lost(self, func_name, reason): self.close() Logging.log('{0}: connection to hsdev lost: {1}'.format(func_name, reason), Logging.LOG_ERROR) # send error to callbacks with self.request_map as requests: for (callbacks, _, _) in requests.values(): callbacks.call_on_error('connection lost', {}) requests.clear()
def run_build(view, project_name, project_dir, config): # Don't build if a build is already running for this project # We compare the project_name for simplicity (projects with same # names are of course possible, but unlikely, so we let them wait) if project_name in PROJECTS_BEING_BUILT: Logging.log( "Waiting for build action on '%s' to complete." % project_name, Logging.LOG_WARNING) Common.show_status_message('Already building %s' % project_name, is_ok=False, priority=5) return # Set project as building PROJECTS_BEING_BUILT.add(project_name) build_tool_name = Settings.PLUGIN.haskell_build_tool if build_tool_name == 'stack' and not is_stack_project( project_dir): # rollback to cabal build_tool_name = 'cabal' tool = BUILD_TOOL[build_tool_name] # Title of tool: Cabal, Stack tool_title = tool['name'] # Title of action: Cleaning, Building, etc. action_title = config['message'] # Tool name: cabal tool_name = tool['command'] # Tool arguments (commands): build, clean, etc. tool_steps = config['steps'][build_tool_name] # Config override override_config = Settings.get_project_setting(view, 'stack_config_file') override_args = [] if override_config: override_args = ['--stack-yaml', override_config] # Assemble command lines to run (possibly multiple steps) commands = [[tool_name] + step + override_args for step in tool_steps] Logging.log('running build commands: {0}'.format(commands), Logging.LOG_TRACE) def done_callback(): # Set project as done being built so that it can be built again PROJECTS_BEING_BUILT.remove(project_name) # Run them ParseOutput.run_chain_build_thread(view, project_dir, '{0} {1} with {2}'.format( action_title, project_name, tool_title), commands, on_done=done_callback)
def update_markers_across_views(self): '''Mark the regions in open views where errors were found. ''' begin_time = time.clock() for win in sublime.windows(): for view in win.views(): self.update_markers_in_view(view) end_time = time.clock() Logging.log('total time to mark {0} diagnostics: {1} seconds'.format(len(self.messages), end_time - begin_time), Logging.LOG_DEBUG)
def call_error(self, err, details): self.log_time() comb_details = ', '.join(['{0}: {1}'.format(k, v) for k, v in details.items()]) Logging.log('{0} returns error: {1}\n{2}'.format(self.command, err, comb_details), Logging.LOG_ERROR) retval = None for err_func in reversed(self.on_error): retval = err_func(err, details) return retval
def __init__(self, project, project_dir, opt_args): if debug_any(): print('Starting \'ghc-mod\' for project {0}'.format(project)) self.ghcmod = None self.action_lock = None self.stderr_drain = None self.cmd = [] self.diag_prefix = 'ghc-mod: project ' + project win = sublime.active_window() msg = 'Error and diagnostic output from ' + self.diag_prefix banner = '~' * len(msg) self.output_panel = Common.output_panel( win, panel_name=self.diag_prefix, text='\n'.join([banner, msg, banner, '', ''])) panel_settings = self.output_panel.settings() panel_settings.set("auto_indent", False) panel_settings.set("smart_indent", False) # if self.exec_with is not None: # if self.exec_with == 'cabal': # cmd += ['cabal'] # elif self.exec_with == 'stack': # cmd += ['stack'] self.cmd += ['ghc-mod'] # if self.exec_with is not None: # cmd += ['--'] self.cmd += [ '-b', '\\n', '--line-prefix', self.GHCMOD_OUTPUT_MARKER + ',' + self.GHCMOD_ERROR_MARKER ] self.cmd += opt_args self.cmd += ['legacy-interactive'] if debug_any(): print('ghc-mod command: {0}'.format(self.cmd)) self.ghcmod = ProcHelper.ProcHelper(self.cmd, cwd=project_dir) if self.ghcmod.process is not None: self.ghcmod.process.stdin = io.TextIOWrapper( self.ghcmod.process.stdin, 'utf-8') self.ghcmod.process.stdout = io.TextIOWrapper( self.ghcmod.process.stdout, 'utf-8') self.action_lock = threading.Lock() self.stderr_drain = OutputCollector.DescriptorDrain( self.diag_prefix, self.ghcmod.process.stderr) self.stderr_drain.start() else: Logging.log('Did not start ghc-mod ({0}) successfully.'.format( self.diag_prefix))
def update_markers_across_views(self): '''Mark the regions in open views where errors were found. ''' begin_time = time.clock() for win in sublime.windows(): for view in win.views(): self.update_markers_in_view(view) end_time = time.clock() Logging.log( 'total time to mark {0} diagnostics: {1} seconds'.format( len(self.messages), end_time - begin_time), Logging.LOG_DEBUG)
def connect_backend(self): Logging.log('Connecting to \'hsdev\' server at {0}:{1}'.format(self.hostname, self.port), Logging.LOG_INFO) retval = True self.client = HsDevClient.HsDevClient(self.backend_mgr) if self.client.connect(self.hostname, self.port): # For a local hsdev server that we started, send the link command so that it exits when we exit. if self.is_local_hsdev: self.link() else: Logging.log('Connections to \'hsdev\' server unsuccessful, see tracebacks to diagnose.', Logging.LOG_ERROR) retval = False return retval
def start_backend(self): retval = True if self.is_local_hsdev: Logging.log('Starting local \'hsdev\' server', Logging.LOG_INFO) use_log_level = (self.version >= [0, 2, 3, 2]) log_config = Settings.PLUGIN.hsdev_log_config log_level = Settings.PLUGIN.hsdev_log_level cmd = self.concat_args([(True, ["hsdev"]), (True, ["run"]), (self.port, ["--port", str(self.port)]), (self.cache, ["--cache", self.cache]), (self.log_file, ["--log", self.log_file]), (not use_log_level and log_config, ["--log-config", log_config]), (use_log_level, ["--log-level", log_level])]) hsdev_proc = ProcHelper.exec_with_wrapper(self.exec_with, self.install_dir, cmd) if hsdev_proc.process is not None: # Use TextIOWrapper here because it combines decoding with newline handling, # which means less to maintain. hsdev_proc.process.stdout = io.TextIOWrapper(hsdev_proc.process.stdout, 'utf-8') hsdev_proc.process.stderr = io.TextIOWrapper(hsdev_proc.process.stderr, 'utf-8') # Read and wait for hsdev's startup messge. 15 seconds should be enough time for the message to appear. # Otherwise, kill the thread because we don't want to get stuck waiting forever. startup_reader = HsDevStartupReader(hsdev_proc.process.stdout) startup_reader.start() startup_reader.wait_startup(15.0) if startup_reader.successful(): port = startup_reader.port() if port != self.port: Logging.log('hsdev: server port changed, was {0}, now {1}'.format(self.port, port), Logging.LOG_WARNING) self.port = port self.drain_stdout = OutputCollector.DescriptorDrain('hsdev stdout', hsdev_proc.process.stdout) self.drain_stderr = OutputCollector.DescriptorDrain('hsdev stderr', hsdev_proc.process.stderr) self.drain_stdout.start() self.drain_stderr.start() self.hsdev_process = hsdev_proc Logging.log('Local \'hsdev\' server started successfully.', Logging.LOG_INFO) else: # This is a bit of a "Hail Mary!" because readline() could just hang forever. Just to make sure, # kill the process too! startup_reader.stop() hsdev_proc.process.kill() if hsdev_proc.process_err is not None: Logging.log('Possible reason for timeout: {0}'.format(hsdev_proc.process_err)) self.hsdev_process = None retval = False sublime.error_message('Timed out waiting for \'hsdev\' to start up.') else: errmsg = 'Could not start local \'hsdev\' server because:\n\n' + hsdev_proc.process_err sublime.error_message(errmsg) self.hsdev_process = None retval = False return retval
def run(self): self.end_event.clear() while not self.end_event.is_set(): srvout = self.stdout.readline().strip() Logging.log('hsdev initial: {0}'.format(srvout), Logging.LOG_DEBUG) if srvout != '': start_confirm = re.search(r'[Ss]erver started at port (?P<port>\d+)$', srvout) if start_confirm: self.hsdev_port = int(start_confirm.group('port')) Logging.log('hsdev initial: \'hsdev\' server started at port {0}'.format(self.hsdev_port)) self.end_event.set() else: # Got EOF, stop loop. self.end_event.set()
def create_connection(self, idx, host, port): for retry in range(1, self.CONNECT_TRIES): try: Logging.log('[pool {0}]: connecting to hsdev server (attempt {1})...'.format(idx, retry), Logging.LOG_INFO) # Use 'localhost' instead of the IP dot-quad for systems (and they exist) that are solely # IPv6. Makes this code friendlier to IPv4 and IPv6 hybrid systems. return socket.create_connection((host, port)) except (socket.gaierror, IOError): # Captures all of the socket exceptions: msg = '[pool {0}]: Failed to connect to hsdev {1}:{2}:'.format(idx, host, port) Logging.log(msg, Logging.LOG_ERROR) print(traceback.format_exc()) time.sleep(self.CONNECT_DELAY) return None
def run_build(self, project_name, project_dir, config): # Don't build if a build is already running for this project # We compare the project_name for simplicity (projects with same # names are of course possible, but unlikely, so we let them wait) if project_name in self.PROJECTS_BEING_BUILT: Logging.log("Waiting for build action on '%s' to complete." % project_name, Logging.LOG_WARNING) Common.sublime_status_message('Already building {0}'.format(project_name)) return # Set project as building self.PROJECTS_BEING_BUILT.add(project_name) Logging.log('project build tool: {0}'.format(Settings.get_project_setting(self.view, 'haskell_build_tool')), Logging.LOG_DEBUG) Logging.log('settings build tool: {0}'.format(Settings.PLUGIN.haskell_build_tool), Logging.LOG_DEBUG) build_tool_name = Settings.get_project_setting(self.view, 'haskell_build_tool', Settings.PLUGIN.haskell_build_tool) if build_tool_name == 'stack' and not self.is_stack_project(project_dir): # rollback to cabal build_tool_name = 'cabal' tool = self.BUILD_TOOL[build_tool_name] # Title of tool: Cabal, Stack tool_title = tool['name'] # Title of action: Cleaning, Building, etc. action_title = config['message'] # Tool name: cabal tool_name = tool['command'] # Tool arguments (commands): build, clean, etc. tool_steps = config['steps'][build_tool_name] # Config override override_args = [] override_config = Settings.get_project_setting(self.view, 'active_stack_config') if tool_name == 'stack' else '' if override_config: override_args = ['--stack-yaml', override_config] # Assemble command lines to run (possibly multiple steps) commands = [[tool_name] + override_args + step if isinstance(step, list) else step for step in tool_steps] Logging.log('running build commands: {0}'.format(commands), Logging.LOG_TRACE) # Run them ## banner = '{0} {1} with {2}\ncommands:\n{3}'.format(action_title, project_name, tool_title, commands) banner = '{0} {1} with {2}'.format(action_title, project_name, tool_title) Logging.log(banner, Logging.LOG_DEBUG) Utils.run_async('wait_for_chain_to_complete', self.wait_for_chain_to_complete, self.view, project_name, project_dir, banner, commands)
def __init__(self, backend_mgr, local=True, port=HSDEV_DEFAULT_PORT, host=HSDEV_DEFAULT_HOST, **kwargs): super().__init__(backend_mgr) Logging.log('{0}.__init__({1}, {2})'.format(type(self).__name__, host, port), Logging.LOG_INFO) # Sanity checking: exec_with = kwargs.get('exec-with') install_dir = kwargs.get('install-dir') if bool(exec_with) ^ bool(install_dir): if install_dir is None: sublime.error_message('\n'.join(['\'exec_with\' requires an \'install_dir\'.', '', 'Please check your \'backends\' configuration and retry.'])) raise RuntimeError('\'exec_with\' requires an \'install_dir\'.') else: sublime.error_message('\n'.join(['\'install_dir\' requires an \'exec_with\'.', '', 'Please check your \'backends\' configuration and retry.'])) raise RuntimeError('\'install_dir\' requires an \'exec_with\'.') elif exec_with and exec_with not in ['stack', 'cabal', 'cabal-new-build']: sublime.error_message('\n'.join(['Invalid backend \'exec_with\': {0}'.format(exec_with), '', 'Valid values are "cabal", "cabal-new-build" or "stack".', 'Please check your \'backends\' configuration and retry.'])) raise RuntimeError('Invalid backend \'exec_with\': {0}'.format(exec_with)) # Local hsdev server process and params self.is_local_hsdev = local self.hsdev_process = None self.cache = os.path.join(Common.sublime_haskell_cache_path(), 'hsdev') self.log_file = os.path.join(Common.sublime_haskell_cache_path(), 'hsdev', 'hsdev.log') self.exec_with = exec_with self.install_dir = Utils.normalize_path(install_dir) if install_dir is not None else None # Keep track of the hsdev version early. Needed to patch command line arguments later. self.version = HsDevBackend.hsdev_version(self.exec_with, self.install_dir) self.drain_stdout = None self.drain_stderr = None # Connection params self.port = port self.hostname = host if self.is_local_hsdev: self.hostname = self.HSDEV_DEFAULT_HOST self.client = None self.serial_lock = threading.RLock() self.request_serial = 1
def make_augmented_path(): ''' Generate the augmented PATH for subprocesses: adds the appropriate cabal/stack local install directory ($HOME/.local/bin for *nix, %APPDATA%/local/bin for Windows) and updates PATH with `add_to_PATH` extras. ''' std_places = [] if Settings.PLUGIN.add_standard_dirs: std_places.append("$HOME/.local/bin" if not Utils.is_windows() else "%APPDATA%/local/bin") if Utils.is_macosx(): std_places.append('$HOME/Library/Haskell/bin') std_places += CabalConfigRdr.cabal_config() std_places = [dir for dir in [Utils.normalize_path(path) for path in std_places] if os.path.isdir(dir)] add_to_path = list(filter(os.path.isdir, map(Utils.normalize_path, Settings.PLUGIN.add_to_path))) Logging.log("std_places = {0}".format(std_places), Logging.LOG_INFO) Logging.log("add_to_PATH = {0}".format(add_to_path), Logging.LOG_INFO) return os.pathsep.join(add_to_path + std_places)
def exec_with_wrapper(exec_with, install_dir, cmd_list): '''Wrapper function for inserting the execution wrapper, e.g., 'cabal exec' or 'stack exec' :returns: Process object from ProcHelper. ''' proc_args = {} if exec_with is not None: cmd_list = exec_wrapper_cmd(exec_with, cmd_list) if install_dir is not None: proc_args['cwd'] = Utils.normalize_path(install_dir) else: raise RuntimeError('ProcHelper.exec_with_wrapper: invalid install_dir (None)') else: cmd = Which.which(cmd_list[0], ProcHelper.get_extended_path()) if cmd is not None: cmd_list[0] = cmd Logging.log('ProcHelper.exec_with_wrapper: {0} in {1}'.format(cmd_list, proc_args.get('cwd')), Logging.LOG_DEBUG) return ProcHelper(cmd_list, **proc_args)
def connect(self, host, port): for i in range(0, self.N_SOCKETS): sock = self.create_connection(i, host, port) if sock is not None: hsconn = HsDevConnection(sock, self, i) self.socket_pool.insert(i, hsconn) else: # Something went wrong, terminate all connnections: for sock in self.socket_pool: try: sock.close() except OSError: pass self.socket_pool = [] return False # We were successful, start all of the socket threads, make them available... for sock in self.socket_pool: sock.start_connection() Logging.log('Connections established to \'hsdev\' server.', Logging.LOG_INFO) return True
def dispatch_response(self, resp): resp_id = resp.get('id') if not resp_id and 'request' in resp: # Error without an id, id is in the 'request' key's content. orig_req = json.loads(resp['request']) resp_id = orig_req.get('id') if resp_id: with self.request_map as requests: reqdata = requests.get(resp_id) if reqdata: callbacks, wait_event, _ = reqdata # Unconditionally call the notify callback: if 'notify' in resp: if Settings.COMPONENT_DEBUG.callbacks: print('id {0}: notify callback'.format(resp_id)) callbacks.call_notify(resp['notify']) if 'result' in resp or 'error' in resp: if wait_event: requests[resp_id] = (callbacks, wait_event, resp) # The wait-er calls callbacks, cleans up after the request. wait_event.set() else: # Enqueue for async receiver: Put the (resp_id, resp, reqdata) tuple onto the queue del requests[resp_id] if 'result' in resp: Utils.run_async('result {0}'.format(resp_id), callbacks.call_response, resp['result']) elif 'error' in resp: err = resp.pop("error") Utils.run_async('error {0}'.format(resp_id), callbacks.call_error, err, resp) else: msg = 'HsDevClient.receiver: request data expected for {0}, none found.'.format(resp_id) Logging.log(msg, Logging.LOG_WARNING) elif Logging.is_log_level(Logging.LOG_ERROR): print('HsDevClient.receiver: request w/o id\n{0}'.format(pprint.pformat(resp)))
def is_available(**kwargs): # Yes, this is slightly redundant because eventually __init__ does the same thing for a class # instance. exec_with = kwargs.get('exec-with') install_dir = kwargs.get('install-dir') local = kwargs.get('local', False) exec_install_set = not bool(exec_with) ^ bool(install_dir) backend_name = kwargs.get('backend_name', 'not specified.') if exec_install_set or local: if not exec_install_set: # Either exec-with or install-dir isn't set, so the corresponding configuration target is unavailable. return False hsdev_ver = HsDevBackend.hsdev_version(exec_with, install_dir) str_version = '.'.join([str(v) for v in hsdev_ver]) Logging.log('hsdev version: {0}'.format(str_version), Logging.LOG_INFO) retval = hsdev_ver >= HsDevBackend.HSDEV_MIN_VER and hsdev_ver < HsDevBackend.HSDEV_MAX_VER if not retval: if retval != HsDevBackend.HSDEV_NOT_FOUND: min_version = '.'.join([str(v) for v in HsDevBackend.HSDEV_MIN_VER]) max_version = '.'.join([str(v) for v in HsDevBackend.HSDEV_MAX_VER]) msg = '\n'.join(['Backend configuration: "{0}"'.format(backend_name), '', 'Incompatible hsdev, detected version ' + str_version, 'Version should be \u2265 ' + min_version + ' and < ' + max_version]) else: msg = '\n'.join(['Backend configuration: "{0}"'.format(backend_name), '', 'Tried executing hsdev to get a version number, not successful.', 'Is hsdev installed (or built, if using stack or cabal exec wrappers)?']) sublime.message_dialog(msg) return retval # Assume that a remote backend is actually available. Ultimately, we might not connect to it, but # it is available to us as a backend. return True
def initialize(self): if self.current_state(self.INITIAL): with self.action_lock: # Can only start a backend iff in the INITIAL state. Logging.log('Starting backend \'{0}\''.format(self.current_backend_name), Logging.LOG_INFO) backend_info = self.possible_backends.get(self.current_backend_name, {}) backend_clazz = self.BACKEND_META.get(backend_info.get('backend') or Backend.NullHaskellBackend.backend_name()) if backend_clazz is not None: the_backend = backend_clazz(self, **backend_info.get('options', {})) else: the_backend = None if the_backend is not None: self.state_startup(the_backend) self.state_connect(the_backend) self.state_active(the_backend) if self.current_state(self.INITIAL): # Something failed during startup, revert to the null backend self.set_backend(Backend.NullHaskellBackend(self)) elif not self.current_state(self.ACTIVE): state_str = self.STATES_TO_NAME.get(self.state, str(self.state)) Logging.log('BackendManager: Invalid state after starting backend: {0}'.format(state_str), Logging.LOG_ERROR)
def reinspect_all(self): Logging.log('reinspect all', Logging.LOG_TRACE) with BackendManager.inspector() as insp: insp.start_inspect()
def set_selected(self, i): if i < 0 or i >= len(self.corrections): Logging.log('AutoFixState.set_selected({0}): out of bound'.format(i), Logging.LOG_ERROR) return self.selected = i self.mark()
def call(self, command, opts, callbacks, wait, timeout): if not self.verify_connected(): return None if wait else False opts = opts or {} opts.update({'no-file': True, 'id': callbacks.ident, 'command': command}) wait_receive = threading.Event() if wait else None if debug_recv(): def client_call_error(exc, details): print('{0}.client_call_error: exc {1} details {2}'.format(type(self).__name__, exc, details)) callbacks.on_error.append(client_call_error) with self.request_map as requests: requests[callbacks.ident] = (callbacks, wait_receive, None) if debug_send(): print(u'HsDevClient.call[{0}] cmd \'{1}\' opts\n{2}'.format(callbacks.ident, command, pprint.pformat(opts))) try: # Randomly choose a connection from the pool -- even if we end up having a stuck sender, this should minimize # the probability of a complete hang. random.choice(self.socket_pool).send_request(opts) retval = True if wait: if debug_send(): print(u'HsDevClient.call: waiting to receive, timeout = {0}.'.format(timeout)) wait_receive.wait(timeout) if debug_send(): print(u'HsDevClient.call: done waiting.') with self.request_map as requests: if wait_receive.is_set(): callbacks, _, resp = requests[callbacks.ident] del requests[callbacks.ident] retval = resp if 'result' in resp: retval = callbacks.call_response(resp['result']) elif 'error' in resp: err = resp.pop("error") retval = callbacks.call_error(err, resp) if Settings.COMPONENT_DEBUG.callbacks: print('HsDevClient.call: wait {0} result {1}'.format(callbacks.ident, retval)) else: req = pprint.pformat(opts) errmsg = 'HsDevClient.call: wait_receive event timed out for id {0}\n{1}'.format(callbacks.ident, req) Logging.log(errmsg, Logging.LOG_ERROR) if Settings.COMPONENT_DEBUG.socket_pool: with self.request_map as request_map: print('id {0} request_map {1}'.format(callbacks.ident, len(request_map))) return retval except OSError: self.connection_lost('call', sys.exc_info()[1]) self.backend_mgr.lost_connection() return False
def log_time(self): Logging.log('{0}: {1} seconds'.format(self.command, self.time()), Logging.LOG_TRACE)