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