Source code for plugins.html_server
#
##
##  SPDX-FileCopyrightText: © 2007-2022 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.1  (Sat May 21 14:49:50 CEST 2022)
##  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 html file viewer
This module provides functionality to view a local html file in the
browser without using the 'file:' transport mechanism.
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 HtmlServer:
    """A specialized Html 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 html server and access the files over
    'http:' protocol. The HtmlServer is very lightweigh 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 html.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 HtmlServer is registered by adding it to the
    list HtmlServer._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.
    Attributes
    ----------
    path: 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: Popen
        The subprocess.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 HtmlServer"""
        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 HtmlServer serving {path}")
            print(f"  running as pid {P.pid} on port {port}")
            HtmlServer._servers.append(self)
        else:
            print(f"Failed creating HtmlServer for {os.getcwd()}")
        self.path = path
        self.port = port
        self.P = P
[docs]    def stop(self):
        """Stop a HtmlServer"""
        print(f"Stopping HtmlServer 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
        HtmlServer._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"HtmlServer on port {self.port} showing url {url}")
        else:
            utils.warn("The HtmlServer 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:`HtmlServer`) to serve an html file
    over the http: protocol to a browser on the local machine.
    The browser command is configurable in the settings.
    Parameters
    ----------
    path: :term:`path_like`
        The path of the file to be displayed. This should normally be a
        file with suffix .html.
    Notes
    -----
    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 HtmlServer and use its connect method
    multiple times.
    """
    path = Path(path)
    if path.is_dir():
        name = ''
    else:
        name = path.name
        path = path.parent
    HtmlServer(path).connect(name) 
if not pf.sphinx:
    # Make sure we stop all servers on exit
    pf.onExit(HtmlServer.stop_all)
# End