Skip to content

Commit

Permalink
feat! built-in http server, socket and http on same port
Browse files Browse the repository at this point in the history
  • Loading branch information
builder555 committed Jan 24, 2024
1 parent b8d3639 commit 8f88992
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 15 deletions.
25 changes: 25 additions & 0 deletions backend/io_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import sys
import os
import sys
import sys

def resource_path(relative_path):
""" Get the absolute path to the resource, works for development and for PyInstaller """
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path: str = getattr(sys, '_MEIPASS', os.path.abspath('.'))
return os.path.join(base_path, relative_path)

def parse_cmd_args(default_host, default_port):

host = sys.argv[1] if len(sys.argv) > 1 else default_host
port = int(sys.argv[2]) if len(sys.argv) > 2 else default_port

if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
print(f"Usage: {sys.argv[0]} [host] [port]")
print(f"Example: {sys.argv[0]} 127.0.0.1 8080")
print(f"Default host: {default_host}")
print(f"Default port: {default_port}")
return None, None

return host,port

17 changes: 10 additions & 7 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ws_server import CommandProcessor, WebSocketHandler
from version_checker import VersionChecker
import logging
from io_utils import parse_cmd_args, resource_path

LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
timestamp_format = "%H:%M:%S"
Expand All @@ -13,25 +14,27 @@
datefmt=timestamp_format,
)

DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8080

async def main(stop_event=asyncio.Event()):
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
host, port = parse_cmd_args(DEFAULT_HOST, DEFAULT_PORT)
if not host or not port:
return

pinesam_url = "https://api.github.com/repos/builder555/PineSAM/releases/latest"
ironos_url = "https://api.github.com/repos/Ralim/IronOS/releases/latest"
version_file = os.path.join(parent_dir, "version.txt")
print(version_file)
app_version_manager = VersionChecker(file_path=version_file, api_url=pinesam_url)
app_version_manager = VersionChecker(api_url=pinesam_url, file_path=resource_path('version.txt'))
ironos_version_manager = VersionChecker(api_url=ironos_url)

pinecil_finder = PinecilFinder()
command_processor = CommandProcessor(
pinecil_finder, app_version_manager, ironos_version_manager
)
ws_handler = WebSocketHandler(command_processor)
ws_handler = WebSocketHandler(command_processor, ui_path=resource_path('gui'))
pinecil_monitor = PinecilMonitor(pinecil_finder, ws_handler.broadcast)

tasks = [
asyncio.create_task(ws_handler.serve("0.0.0.0", 12999)),
asyncio.create_task(ws_handler.serve(host, port)),
asyncio.create_task(pinecil_monitor.monitor(stop_event)),
]
await asyncio.gather(*tasks)
Expand Down
13 changes: 12 additions & 1 deletion backend/test_version_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,54 @@
from version_checker import VersionChecker
from unittest.mock import MagicMock, mock_open


@pytest.fixture
def mock_file_not_exist(monkeypatch):
monkeypatch.setattr("os.path.exists", lambda path: False)


@pytest.fixture
def mock_file_exist(monkeypatch):
monkeypatch.setattr("os.path.exists", lambda path: True)
monkeypatch.setattr("builtins.open", mock_open(read_data="1.0.0"))


@pytest.fixture
def mock_api_response(monkeypatch):
mock_response = MagicMock()
mock_response.json.return_value = {"tag_name": "v2.0.0"}
monkeypatch.setattr("requests.get", lambda url: mock_response)


@pytest.fixture
def mock_api_bad_response(monkeypatch):
mock_response = MagicMock()
mock_response.json.return_value = {"error": "invalid"}
monkeypatch.setattr("requests.get", lambda url: mock_response)


def test_read_version_empty_path():
vc = VersionChecker()
assert vc.read_version() == ""


def test_read_version_non_existent_file(mock_file_not_exist):
vc = VersionChecker(file_path="nonexistent/path")
assert vc.read_version() == ""


def test_read_version_existing_file(mock_file_exist):
vc = VersionChecker(file_path="path/to/version_file")
assert vc.read_version() == "1.0.0"

def test_get_latest_version_with_bad_api_response_returns_file_version(mock_file_exist, mock_api_bad_response):

def test_get_latest_version_with_bad_api_response_returns_file_version(
mock_file_exist, mock_api_bad_response
):
vc = VersionChecker(file_path="path/to/version_file")
assert vc.get_latest_version() == "1.0.0"


def test_get_latest_version_with_api_url(mock_api_response):
vc = VersionChecker(api_url="http://api.url")
assert vc.get_latest_version() == "2.0.0"
45 changes: 43 additions & 2 deletions backend/ws_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,55 @@ def is_semver_greater(v1, v2):
return v1 > v2


import http
import os
import mimetypes
from websockets import WebSocketServerProtocol


def make_protocol(ui_path: str):
class HTTPServerProtocol(WebSocketServerProtocol):
async def process_request(self, path, request_headers):
is_ws_request = "Upgrade" in request_headers
if not is_ws_request:
return self._handle_http_request(path)

def _get_content_type(self, filepath):
"""
Returns the MIME type based on the file extension.
"""
content_type, _ = mimetypes.guess_type(filepath)
return content_type or "application/octet-stream"

def _handle_http_request(self, path):
if path == "/" or path == "":
path = "/index.html"
filepath = os.path.join(ui_path, path.lstrip("/"))
if os.path.exists(filepath) and os.path.isfile(filepath):
content_type = self._get_content_type(filepath)
with open(filepath, "rb") as f:
content = f.read()
response_headers = [
("Content-Type", content_type),
("Content-Length", str(len(content))),
]
return http.HTTPStatus.OK, response_headers, content
else:
return http.HTTPStatus.NOT_FOUND, [], b"404 Not Found"

return HTTPServerProtocol

class WebSocketHandler:
def __init__(self, command_processor: CommandProcessor):
def __init__(self, command_processor: CommandProcessor, ui_path: str = './ui'):
self.command_processor = command_processor
self.clients = set()
self._ui_path = ui_path

async def serve(self, host: str, port: int):
logging.info(f"Starting websocket server on {host}:{port}")
await websockets.serve(self.ws_handler, host, port)
await websockets.serve(
self.ws_handler, host, port, create_protocol=make_protocol(self._ui_path)
)

async def handle_message(
self, websocket: websockets.WebSocketServerProtocol, data: Data
Expand Down
6 changes: 4 additions & 2 deletions ci/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ Push-Location ui
npm run build
Pop-Location

pyinstaller backend/main_server.py
pyinstaller --add-data "ui/dist;ui" ui/serve.py
pyinstaller --onefile --add-data "./version.txt;/" --add-data "./ui/dist;/ui" backend/main.py

# pyinstaller backend/main_server.py
# pyinstaller --add-data "ui/dist;ui" ui/serve.py
5 changes: 3 additions & 2 deletions ci/build.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/bin/bash

set -e

pushd ui || exit 2
npm run build
popd || exit 2

pyinstaller backend/main_server.py --collect-submodules dbus_fast
pyinstaller --add-data ui/dist:ui ui/serve.py
pyinstaller --onefile --add-data "./version.txt:/" --add-data "./ui/dist:/gui" backend/main.py --collect-submodules dbus_fast
6 changes: 5 additions & 1 deletion ui/src/socket.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// import process from 'process';
class Socket {
constructor() {
this.socket = null;
Expand All @@ -8,8 +9,11 @@ class Socket {
console.warn(message);
}
initSocket() {
// change this if you're running back-end on a different port when in development
let port = import.meta.env.MODE === 'production' ? window.location.port : 8080;

return new Promise((resolve) => {
this.socket = new WebSocket(`ws://${window.location.hostname}:12999/`);
this.socket = new WebSocket(`ws://${window.location.hostname}:${port}/`);
this.registerCallbacks();
this.socket.onopen = () => {
resolve();
Expand Down

0 comments on commit 8f88992

Please sign in to comment.