Source code for plugins.http_server

#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##
"""Local http server and html file viewer

This module provides functionality to view a local html file in the
browser using the 'http:' transport mechanism instead of 'file:'.
It was created to allow viewing WebGL models from a local directory.
"""
import os

import pyformex as pf
from pyformex import utils
from pyformex.path import Path


[docs]class HttpServer: """A specialized http server to serve local files. This server is intended to serve local files to a browser. It is meant as a replacement for the 'file:' transport mechanism. For security reasons modern browsers often do not allow to include files (especially script types) from another origin. With the file: protocol any other file, even in the same directory, may be considered as a foreign origin. A CORS error is raised in such cases. The solution is to use a local http server and access the files over 'http:' protocol. The HttpServer is very lightweight class which can serve a directory and all its files and subdirectories to the local machine. It is not intended to be exposed directly to the network. It uses the :class:`http.server` from the Python standard library. Parameters ---------- path: :term:`path_like` The path of the local directory to be served. The user should have read access to this directory. port: int | None The TCP port on which the server will be listening. This should be an unused port number in the high rang (>= 1024). If not provided, a random free port number will be used. Every successfully created HttpServer is registered by adding it to the list HttpServer._servers. When pyFormex exits, all these servers will be topped. The user can stop a server at any time though. If you want a server to continue after pyFormex exits, remove it from the list. The following attributes of the HttpServer provide useful information: path: :class:`Path` The path of the directory with accessible files. port: int: The port number on which the server is listening. In your browser, use ``http://localhost:PORT/SOMEFILE`` to view the contents of SOMEFILE. P: :class:`subprocess.Popen` The Popen instance of the running server. Its attribute P.pid gives the process id of the server. """ _servers = [] # registers the instances def __init__(self, path, port=None): """Initialize the HttpServer""" path = Path(path) if not path.is_dir(): raise ValueError("path should be a directory") os.chdir(path) if port is None: port = get_free_socket() P = utils.system(f'python3 -m http.server {port}', wait=False) if P.poll() is None: # The server is running print(f"Created new HttpServer serving {path}") print(f" running as pid {P.pid} on port {port}") HttpServer._servers.append(self) else: print(f"Failed creating HttpServer for {os.getcwd()}") self.path = path self.port = port self.P = P
[docs] def stop(self): """Stop a HttpServer""" print(f"Stopping HttpServer pid {self.P.pid} on port {self.port}") P = self.P print(P, P.pid) P.terminate() try: print("waiting") P.wait(timeout=5) except TimeoutExpired: P.kill() try: P.wait(timeout=5) except TimeoutExpired: pass HttpServer._servers.remove(self) return P
[docs] @classmethod def stop_all(cls): """Stop all running servers""" while len(cls._servers) > 0: cls._servers[0].stop()
[docs] def connect(self, url='', browser=None): """Show an url in the browser. Parameters ---------- url: :term:`path_like` The path of the file to be shown in the browser. The path is relative to the served directory path. An empty string or a single '/' will serve the directory itself, showing the contents of the directory. browser: str The name of the browser command. If not provided, the value from the settings is used. It can be configured in the Settings menu. """ if self.P.poll() is None: utils.system(f"{pf.cfg['browser']} localhost:{self.port}/{url}", wait=False) print(f"HttpServer on port {self.port} showing url {url}") else: utils.warn("The HttpServer has stopped")
[docs]def get_free_socket(): """Find and return a random free port number. A random free port number in the upper range 1024-65535 is found. The port is immediately bound with the reuse option set. This avoids a race condition (where another process could bind to the port before we had the change to do so) while still keeping the port bindable for our purpose. """ import socket sock = socket.socket() sock.bind(('', 0)) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) return sock.getsockname()[1]
[docs]def showHtml(path): """Show a local .html file in the browser. Creates a local web server (:class:`HttpServer`) to serve an html file over the http: protocol to a browser on the local machine. The browser command is configurable in the settings. This is a convenient wrapper function if you have a single file to show. If you need to show multiple files from the same directory, you may want to create a single :class:`HttpServer` for the directory and\ use multiple calls to its :meth:`~HttpServer.connect` method. Parameters ---------- path: :term:`path_like` The path of the file to be displayed. This should normally be a file with suffix ``.html``. """ path = Path(path) if path.is_dir(): name = '' else: name = path.name path = path.parent HttpServer(path).connect(name)
if not pf.sphinx: # Make sure we stop all servers on exit pf.onExit(HttpServer.stop_all) # End