diff --git a/README.md b/README.md index 2ba23d26..d96494f0 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `no_such_file` – creates missing directories with `mv` and `cp` commands; * `open` – either prepends `http://` to address passed to `open` or create a new file or directory and passes it to `open`; * `pip_unknown_command` – fixes wrong `pip` commands, for example `pip instatl/pip install`; +* `port_already_in_use` – kills process that bound port; * `python_command` – prepends `python` when you trying to run not executable/without `./` python script; * `python_execute` – appends missing `.py` when executing Python files; * `quotation_marks` – fixes uneven usage of `'` and `"` when containing args'; diff --git a/tests/rules/test_port_already_in_use.py b/tests/rules/test_port_already_in_use.py new file mode 100644 index 00000000..ff85b665 --- /dev/null +++ b/tests/rules/test_port_already_in_use.py @@ -0,0 +1,101 @@ +from io import BytesIO + +import pytest +from thefuck.rules.port_already_in_use import match, get_new_command +from tests.utils import Command + +outputs = [ + ''' + +DE 70% 1/1 build modulesevents.js:141 + throw er; // Unhandled 'error' event + ^ + +Error: listen EADDRINUSE 127.0.0.1:8080 + at Object.exports._errnoException (util.js:873:11) + at exports._exceptionWithHostPort (util.js:896:20) + at Server._listen2 (net.js:1250:14) + at listen (net.js:1286:10) + at net.js:1395:9 + at GetAddrInfoReqWrap.asyncCallback [as callback] (dns.js:64:16) + at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:83:10) + + ''', + ''' +[6:40:01 AM] Building Dependency Graph +[6:40:01 AM] Crawling File System + ERROR Packager can't listen on port 8080 +Most likely another process is already using this port +Run the following command to find out which process: + + lsof -n -i4TCP:8080 + +You can either shut down the other process: + + kill -9 + +or run packager on different port. + + ''', + ''' +Traceback (most recent call last): + File "/usr/lib/python3.5/runpy.py", line 184, in _run_module_as_main + "__main__", mod_spec) + File "/usr/lib/python3.5/runpy.py", line 85, in _run_code + exec(code, run_globals) + File "/home/nvbn/exp/code_view/server/code_view/main.py", line 14, in + web.run_app(app) + File "/home/nvbn/.virtualenvs/code_view/lib/python3.5/site-packages/aiohttp/web.py", line 310, in run_app + backlog=backlog)) + File "/usr/lib/python3.5/asyncio/base_events.py", line 373, in run_until_complete + return future.result() + File "/usr/lib/python3.5/asyncio/futures.py", line 274, in result + raise self._exception + File "/usr/lib/python3.5/asyncio/tasks.py", line 240, in _step + result = coro.send(None) + File "/usr/lib/python3.5/asyncio/base_events.py", line 953, in create_server + % (sa, err.strerror.lower())) +OSError: [Errno 98] error while attempting to bind on address ('0.0.0.0', 8080): address already in use +Task was destroyed but it is pending! +task: wait_for=> + ''' +] + +lsof_stdout = b'''COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME +node 18233 nvbn 16u IPv4 557134 0t0 TCP localhost:http-alt (LISTEN) +''' + + +@pytest.fixture(autouse=True) +def lsof(mocker): + patch = mocker.patch('thefuck.rules.port_already_in_use.Popen') + patch.return_value.stdout = BytesIO(lsof_stdout) + return patch + + +@pytest.mark.usefixtures('no_memoize') +@pytest.mark.parametrize( + 'command', + [Command('./app', stdout=output) for output in outputs] + + [Command('./app', stderr=output) for output in outputs]) +def test_match(command): + assert match(command) + + +@pytest.mark.usefixtures('no_memoize') +@pytest.mark.parametrize('command, lsof_output', [ + (Command('./app'), lsof_stdout), + (Command('./app', stdout=outputs[1]), b''), + (Command('./app', stderr=outputs[2]), b'')]) +def test_not_match(lsof, command, lsof_output): + lsof.return_value.stdout = BytesIO(lsof_output) + + assert not match(command) + + +@pytest.mark.parametrize( + 'command', + [Command('./app', stdout=output) for output in outputs] + + [Command('./app', stderr=output) for output in outputs]) +def test_get_new_command(command): + assert get_new_command(command) == 'kill 18233 && ./app' diff --git a/thefuck/rules/port_already_in_use.py b/thefuck/rules/port_already_in_use.py new file mode 100644 index 00000000..43171d6d --- /dev/null +++ b/thefuck/rules/port_already_in_use.py @@ -0,0 +1,39 @@ +import re +from subprocess import Popen, PIPE +from thefuck.utils import memoize +from thefuck.shells import shell + +patterns = [r"bind on address \('.*', (?P\d+)\)", + r'Unable to bind [^ ]*:(?P\d+)', + r"can't listen on port (?P\d+)", + r'listen EADDRINUSE [^ ]*:(?P\d+)'] + + +@memoize +def _get_pid_by_port(port): + proc = Popen(['lsof', '-i', ':{}'.format(port)], stdout=PIPE) + lines = proc.stdout.read().decode().split('\n') + if len(lines) > 1: + return lines[1].split()[1] + else: + return None + + +@memoize +def _get_used_port(command): + for pattern in patterns: + matched = (re.search(pattern, command.stderr) + or re.search(pattern, command.stdout)) + if matched: + return matched.group('port') + + +def match(command): + port = _get_used_port(command) + return port and _get_pid_by_port(port) + + +def get_new_command(command): + port = _get_used_port(command) + pid = _get_pid_by_port(port) + return shell.and_(u'kill {}'.format(pid), command.script)