diff --git a/CHANGELOG.md b/CHANGELOG.md index f8795c6..55a71a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +* Support config file in `$XDG_CONFIG_HOME/brotab/brotab.env`: +```env +HTTP_IFACE=0.0.0.0 +MIN_HTTP_PORT=4625 +MAX_HTTP_PORT=4635 +``` + This is useful if you want to change interface mediator is binding to. + 1.4.1 (2022-05-29) * Better syntax for navigate and update: diff --git a/brotab/api.py b/brotab/api.py index 23b0d69..b88fa58 100644 --- a/brotab/api.py +++ b/brotab/api.py @@ -16,9 +16,9 @@ from urllib.request import Request from urllib.request import urlopen +from brotab.env import http_iface from brotab.inout import MultiPartForm from brotab.inout import edit_tabs_in_editor -from brotab.mediator.const import DEFAULT_HTTP_IFACE from brotab.operations import infer_all_commands from brotab.parallel import call_parallel from brotab.tab import parse_tab_lines @@ -302,7 +302,7 @@ def api_must_ready(port: int, browser: str, prefix='a', client_timeout: float = 0.1, startup_timeout: float = 1.0) -> SingleMediatorAPI: - client = HttpClient(DEFAULT_HTTP_IFACE, port, timeout=client_timeout) + client = HttpClient(http_iface(), port, timeout=client_timeout) api = SingleMediatorAPI(prefix=prefix, port=port, startup_timeout=startup_timeout, client=client) assert api.browser == browser return api @@ -312,6 +312,7 @@ def int_tab_id(tab_id: str) -> int: """Convert from str(b.20.123) to int(123)""" return int(tab_id.split('.')[-1]) + class MultipleMediatorsAPI(object): """ This API is designed to work with multiple mediators. diff --git a/brotab/env.py b/brotab/env.py new file mode 100644 index 0000000..a9ac7b6 --- /dev/null +++ b/brotab/env.py @@ -0,0 +1,36 @@ +from os import environ +from os.path import exists +from os.path import expanduser + +from brotab.files import slurp_lines +from brotab.mediator.const import DEFAULT_HTTP_IFACE +from brotab.mediator.const import DEFAULT_MAX_HTTP_PORT +from brotab.mediator.const import DEFAULT_MIN_HTTP_PORT +from brotab.mediator.log import mediator_logger + +CONFIG = environ.get('XDG_CONFIG_HOME', expanduser('~/.config')) +DEFAULT_FILENAME = '{0}/brotab/brotab.env'.format(CONFIG) + + +def http_iface(): + return environ.get('HTTP_IFACE', DEFAULT_HTTP_IFACE) + + +def min_http_port(): + return environ.get('MIN_HTTP_PORT', DEFAULT_MIN_HTTP_PORT) + + +def max_http_port(): + return environ.get('MAX_HTTP_PORT', DEFAULT_MAX_HTTP_PORT) + + +def load_dotenv(filename=None): + if filename is None: filename = DEFAULT_FILENAME + mediator_logger.info('Loading .env file: %s', filename) + if not exists(filename): + mediator_logger.info('No .env file found: %s', filename) + return + for line in slurp_lines(filename): + if not line or line.startswith('#'): continue + key, value = line.split('=', 1) + environ[key] = value diff --git a/brotab/files.py b/brotab/files.py new file mode 100644 index 0000000..468953b --- /dev/null +++ b/brotab/files.py @@ -0,0 +1,22 @@ +import os +import tempfile + + +def slurp(filename): + with open(filename) as file_: + return file_.read() + + +def slurp_lines(filename): + with open(filename) as file_: + return [line.strip() for line in file_.readlines()] + + +def spit(filename, contents): + with open(filename, 'w', encoding='utf-8') as file_: + file_.write(contents) + + +def in_temp_dir(filename) -> str: + temp_dir = tempfile.gettempdir() + return os.path.join(temp_dir, filename) diff --git a/brotab/inout.py b/brotab/inout.py index ad963eb..92d923d 100644 --- a/brotab/inout.py +++ b/brotab/inout.py @@ -3,8 +3,8 @@ import os import socket import sys -import tempfile import uuid +from select import select from subprocess import CalledProcessError from subprocess import check_call from tempfile import NamedTemporaryFile @@ -12,36 +12,13 @@ from typing import Iterable from typing import Union -from select import select - +from brotab.env import max_http_port +from brotab.env import min_http_port from brotab.platform import get_editor -MIN_MEDIATOR_PORT = 4625 -MAX_MEDIATOR_PORT = MIN_MEDIATOR_PORT + 10 - - -def slurp(filename): - with open(filename) as file_: - return file_.read() - - -def slurp_lines(filename): - with open(filename) as file_: - return [line.strip() for line in file_.readlines()] - - -def spit(filename, contents): - with open(filename, 'w', encoding='utf-8') as file_: - file_.write(contents) - - -def in_temp_dir(filename) -> str: - temp_dir = tempfile.gettempdir() - return os.path.join(temp_dir, filename) - def get_mediator_ports() -> Iterable: - return range(MIN_MEDIATOR_PORT, MAX_MEDIATOR_PORT) + return range(min_http_port(), max_http_port()) def get_available_tcp_port(start=1025, end=65536, host='127.0.0.1'): diff --git a/brotab/main.py b/brotab/main.py index 46a42c0..70d30e3 100644 --- a/brotab/main.py +++ b/brotab/main.py @@ -69,7 +69,7 @@ from brotab.const import DEFAULT_GET_WORDS_JOIN_WITH from brotab.const import DEFAULT_GET_WORDS_MATCH_REGEX from brotab.inout import get_mediator_ports -from brotab.inout import in_temp_dir +from brotab.files import in_temp_dir from brotab.inout import is_port_accepting_connections from brotab.inout import marshal from brotab.inout import read_stdin diff --git a/brotab/mediator/brotab_mediator.py b/brotab/mediator/brotab_mediator.py index 30e50c0..7236bc4 100755 --- a/brotab/mediator/brotab_mediator.py +++ b/brotab/mediator/brotab_mediator.py @@ -6,10 +6,11 @@ import re import socket +from brotab.env import http_iface +from brotab.env import load_dotenv from brotab.inout import get_mediator_ports from brotab.inout import is_port_accepting_connections from brotab.mediator import sig -from brotab.mediator.const import DEFAULT_HTTP_IFACE from brotab.mediator.const import DEFAULT_SHUTDOWN_POLL_INTERVAL from brotab.mediator.http_server import MediatorHttpServer from brotab.mediator.log import disable_click_echo @@ -66,12 +67,13 @@ def mediator_main(): monkeypatch_socket_bind_allow_port_reuse() disable_click_echo() + load_dotenv() port_range = list(get_mediator_ports()) transport = default_transport() # transport = transport_with_timeout(sys.stdin.buffer, sys.stdout.buffer, DEFAULT_TRANSPORT_TIMEOUT) # transport = transport_with_timeout(sys.stdin.buffer, sys.stdout.buffer, 1.0) remote_api = default_remote_api(transport) - host = DEFAULT_HTTP_IFACE + host = http_iface() poll_interval = DEFAULT_SHUTDOWN_POLL_INTERVAL for port in port_range: diff --git a/brotab/mediator/log.py b/brotab/mediator/log.py index d9a6604..a349a49 100644 --- a/brotab/mediator/log.py +++ b/brotab/mediator/log.py @@ -2,7 +2,7 @@ import logging.handlers from traceback import format_stack -from brotab.inout import in_temp_dir +from brotab.files import in_temp_dir def _init_logger(tag, filename: str): diff --git a/brotab/tests/test_integration.py b/brotab/tests/test_integration.py index faba530..7a69efc 100644 --- a/brotab/tests/test_integration.py +++ b/brotab/tests/test_integration.py @@ -13,6 +13,7 @@ import pytest from brotab.api import api_must_ready +from brotab.env import min_http_port from brotab.inout import get_available_tcp_port from brotab.inout import wait_net_service from brotab.mediator.const import DEFAULT_MIN_HTTP_PORT @@ -144,7 +145,7 @@ def __init__(self): self._browser = Popen(self.CMD, shell=True, cwd=self.CWD, preexec_fn=os.setsid) print('PID', self._browser.pid) - wait_net_service('localhost', DEFAULT_MIN_HTTP_PORT, TIMEOUT) + wait_net_service('localhost', min_http_port(), TIMEOUT) print('init done PID', self._browser.pid) def stop(self): diff --git a/brotab/tests/test_main.py b/brotab/tests/test_main.py index 3524f29..cd0d82b 100644 --- a/brotab/tests/test_main.py +++ b/brotab/tests/test_main.py @@ -6,13 +6,13 @@ from uuid import uuid4 from brotab.api import SingleMediatorAPI -from brotab.inout import MIN_MEDIATOR_PORT +from brotab.env import http_iface +from brotab.env import min_http_port +from brotab.files import in_temp_dir +from brotab.files import spit from brotab.inout import get_available_tcp_port -from brotab.inout import in_temp_dir -from brotab.inout import spit from brotab.main import create_clients from brotab.main import run_commands -from brotab.mediator.const import DEFAULT_HTTP_IFACE from brotab.mediator.http_server import MediatorHttpServer from brotab.mediator.remote_api import default_remote_api from brotab.mediator.transport import Transport @@ -59,7 +59,7 @@ def __init__(self, prefix='a', port=None, remote_api=None): self.port = get_available_tcp_port() if port is None else port self.transport = MockedLoggingTransport() self.remote_api = default_remote_api(self.transport) if remote_api is None else remote_api - self.server = MediatorHttpServer(DEFAULT_HTTP_IFACE, self.port, self.remote_api, 0.050) + self.server = MediatorHttpServer(http_iface(), self.port, self.remote_api, 0.050) self.thread = self.server.run.in_thread() self.transport.received_extend(['mocked']) self.api = SingleMediatorAPI(prefix, port=self.port, startup_timeout=1) @@ -128,7 +128,7 @@ def run_mocked_mediators(count, default_port_offset, delay): """ assert count > 0 print('Creating %d mediators' % count) - start_port = MIN_MEDIATOR_PORT + default_port_offset + start_port = min_http_port() + default_port_offset ports = range(start_port, start_port + count) mediators = [MockedMediator(letter, port, DummyBrowserRemoteAPI()) for i, letter, port in zip(range(count), ascii_letters, ports)] @@ -147,7 +147,7 @@ def run_mocked_mediator_current_thread(port): """ remote_api = DummyBrowserRemoteAPI() port = get_available_tcp_port() if port is None else port - server = MediatorHttpServer(DEFAULT_HTTP_IFACE, port, remote_api, 0.050) + server = MediatorHttpServer(http_iface(), port, remote_api, 0.050) server.run.here() diff --git a/brotab/tests/test_mediator.py b/brotab/tests/test_mediator.py deleted file mode 100644 index 1fa9a25..0000000 --- a/brotab/tests/test_mediator.py +++ /dev/null @@ -1,138 +0,0 @@ -import os -import signal -from contextlib import contextmanager -from multiprocessing import Barrier -from multiprocessing import Process -from unittest import TestCase - -from brotab.api import api_must_ready -from brotab.inout import get_available_tcp_port -from brotab.mediator import sig -from brotab.mediator.const import DEFAULT_HTTP_IFACE -from brotab.mediator.http_server import MediatorHttpServer -from brotab.mediator.log import mediator_logger -from brotab.mediator.remote_api import default_remote_api -# from brotab.mediator.transport import transport_with_timeout -from brotab.wait import Waiter - - -# class MockedPiperMediator: -# def __init__(self, prefix='a', port=None, remote_api=None): -# mediator_logger.info('starting mediator pid=%s', os.getpid()) -# self.prefix = prefix -# self.port = get_available_tcp_port() if port is None else port -# input_r, input_w = os.pipe() -# output_r, output_w = os.pipe() -# self.transport_browser = transport_with_timeout(output_r, input_w, 0.050) -# self.transport_mediator = transport_with_timeout(input_r, output_w, 0.050) -# self.remote_api = default_remote_api(self.transport_mediator) if remote_api is None else remote_api -# self.server = MediatorHttpServer(DEFAULT_HTTP_IFACE, self.port, self.remote_api, poll_interval=0.050) -# self.thread = None -# self.api = None -# -# def start(self): -# self.thread = self.server.run.in_thread() -# self.transport_browser.send('mocked') -# -# def wait_api_ready(self): -# self.api = api_must_ready(port=self.port, browser='mocked', prefix=self.prefix, -# client_timeout=0.1, startup_timeout=1) -# -# def join(self): -# # sig.setup(lambda: server.run.shutdown(join=False)) -# self.server.run.parent_watcher(self.thread.is_alive, interval=0.050) # this is crucial -# self.thread.join() -# -# def __enter__(self): -# return self -# -# def __exit__(self, type_, value, tb): -# self.join() -# -# -# class TestMediatorThreadTerminates(TestCase): -# def setUp(self): -# self.mediator = MockedPiperMediator() -# self.mediator.start() -# self.mediator.wait_api_ready() -# -# def tearDown(self): -# pass -# -# def test_if_cannot_read(self): -# self.mediator.api.list_tabs([]) -# self.mediator.join() # this should complete without manual shutdown -# -# def test_if_cannot_write(self): -# self.mediator.transport_browser.send(['1.1\ttitle\turl']) # make reads work -# self.mediator.transport_browser.close() -# self.mediator.api.list_tabs([]) -# self.mediator.join() # this should complete without manual shutdown -# -# -# class TestMediatorProcessTerminates(TestCase): -# def test_when_parent_died(self): -# port = get_available_tcp_port() -# mediator_logger.info('starting test pid=%s', os.getpid()) -# kill_parent = Barrier(2) -# -# def run_threaded_mediator(): -# mediator = MockedPiperMediator(port=port) -# mediator.start() -# mediator.join() -# -# def run_doomed_parent_browser(): -# mediator_logger.info('doomed_parent_browser pid=%s', os.getpid()) -# mediator_process = Process(target=run_threaded_mediator) -# mediator_process.start() -# mediator_process.join() -# -# def on_sig_child(signum, frame): -# pid, status = os.wait() -# mediator_logger.info('reaped child signum=%s pid=%s status=%s', signum, pid, status) -# -# def run_supervisor(): -# signal.signal(signal.SIGCHLD, on_sig_child) -# doomed_parent_browser = Process(target=run_doomed_parent_browser) -# doomed_parent_browser.start() -# kill_parent.wait() -# doomed_parent_browser.terminate() -# doomed_parent_browser.join() -# -# signal.signal(signal.SIGCHLD, on_sig_child) -# supervisor = Process(target=run_supervisor) -# supervisor.start() -# api = api_must_ready(port, 'mocked') -# -# # kill parent and expect mediator to terminate as well -# kill_parent.wait() -# self.assertTrue(Waiter(api.pid_not_ready).wait(timeout=1.0)) -# supervisor.join() -# -# @contextmanager -# def run_as_child_process(self): -# port = get_available_tcp_port() -# mediator_logger.info('starting test pid=%s', os.getpid()) -# -# def run_threaded_mediator(): -# mediator = MockedPiperMediator(port=port) -# mediator.start() -# sig.setup(lambda: mediator.server.run.shutdown(join=False)) -# mediator.join() -# -# mediator_process = Process(target=run_threaded_mediator) -# mediator_process.start() -# api = api_must_ready(port, 'mocked') -# -# yield mediator_process -# -# self.assertTrue(Waiter(api.pid_not_ready).wait(timeout=1.0)) -# mediator_process.join() -# -# def test_when_sigint_received(self): -# with self.run_as_child_process() as mediator_process: -# os.kill(mediator_process.pid, signal.SIGINT) -# -# def test_when_sigterm_received(self): -# with self.run_as_child_process() as mediator_process: -# os.kill(mediator_process.pid, signal.SIGTERM)