Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions taipy/gui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,12 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover
elif key == "port" and str(value).strip() == "auto":
config["port"] = "auto"
else:
config[key] = value if config.get(key) is None else type(config.get(key))(value) # type: ignore[reportCallIssue]
config[key] = value if config.get(key) is None else type(config.get(key))(
value) # type: ignore[reportCallIssue]
except Exception as e:
_warn(
f"Invalid keyword arguments value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", # noqa: E501
e,
f"Invalid keyword arguments value in Gui.run(): {key} - {value}. "
f"Unable to parse value to the correct type: {e}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passing the exception as an argument is preferred

)
# Load config from env file
if os.path.isfile(env_file_abs_path):
Expand All @@ -273,11 +274,12 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover
if isinstance(config[key], bool):
config[key] = _is_true(value)
else:
config[key] = value if config[key] is None else type(config[key])(value) # type: ignore[reportCallIssue]
config[key] = value if config[key] is None else type(config[key])(
value) # type: ignore[reportCallIssue]
except Exception as e:
_warn(
f"Invalid env value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", # noqa: E501
e,
f"Invalid env value in Gui.run(): {key} - {value}. "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idem

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idem

f"Unable to parse value to the correct type: {e}",
)

# Taipy-config
Expand Down
77 changes: 60 additions & 17 deletions taipy/gui/servers/flask/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from __future__ import annotations

import contextlib
import logging
import os
import pathlib
Expand Down Expand Up @@ -142,6 +141,7 @@ def _get_default_handler(
base_url: str,
) -> Blueprint:
taipy_bp = Blueprint("Taipy", __name__, static_folder=static_folder, template_folder=template_folder)

# Serve static react build

@taipy_bp.route("/", defaults={"path": ""})
Expand Down Expand Up @@ -171,11 +171,13 @@ def my_index(path):
)
except Exception:
raise RuntimeError(
"Something is wrong with the taipy-gui front-end installation. Check that the js bundle has been properly built (is Node.js installed?)." # noqa: E501
"Something is wrong with the taipy-gui front-end installation. "
"Check that the js bundle has been properly built (is Node.js installed?)."
) from None

if path == "taipy.status.json":
return self.direct_render_json(self._gui._serve_status(pathlib.Path(template_folder) / path)) # type: ignore[attr-defined]
return self.direct_render_json(
self._gui._serve_status(pathlib.Path(template_folder) / path)) # type: ignore[attr-defined]
if (file_path := str(os.path.normpath((base_path := static_folder + os.path.sep) + path))).startswith(
base_path
) and os.path.isfile(file_path):
Expand All @@ -185,25 +187,26 @@ def my_index(path):
if (
path.startswith(f"{k}/")
and (
file_path := str(os.path.normpath((base_path := v + os.path.sep) + path[len(k) + 1 :]))
).startswith(base_path)
file_path := str(os.path.normpath((base_path := v + os.path.sep) + path[len(k) + 1:]))
).startswith(base_path)
and os.path.isfile(file_path)
):
return send_from_directory(base_path, path[len(k) + 1 :])
return send_from_directory(base_path, path[len(k) + 1:])
if (
hasattr(__main__, "__file__")
and (
file_path := str(
os.path.normpath((base_path := os.path.dirname(__main__.__file__) + os.path.sep) + path)
)
).startswith(base_path)
file_path := str(
os.path.normpath((base_path := os.path.dirname(__main__.__file__) + os.path.sep) + path)
)
).startswith(base_path)
and os.path.isfile(file_path)
and not self._is_ignored(file_path)
):
return send_from_directory(base_path, path)
if (
(
file_path := str(os.path.normpath((base_path := self._gui._root_dir + os.path.sep) + path)) # type: ignore[attr-defined]
file_path := str(os.path.normpath((base_path := self._gui._root_dir + os.path.sep) + path))
# type: ignore[attr-defined]
).startswith(base_path)
and os.path.isfile(file_path)
and not self._is_ignored(file_path)
Expand Down Expand Up @@ -237,7 +240,15 @@ def test_request_context(self, path, data=None):

def _run_notebook(self):
self._is_running = True
self._ws.run(self._server, host=self._host, port=self._port, debug=False, use_reloader=False)
self._ws.run(
self._server,
host=self._host,
port=self._port,
debug=False,
use_reloader=False,
allow_unsafe_werkzeug=True,
log_output=True
)

def _get_async_mode(self) -> str:
return self._ws.async_mode # type: ignore[attr-defined]
Expand Down Expand Up @@ -380,9 +391,14 @@ def run(
if not self.is_running_from_reloader() and self._gui._get_config("run_browser", False): # type: ignore[attr-defined]
webbrowser.open(client_url or server_url, new=2)
if _is_in_notebook() or run_in_thread:
self._thread = KThread(target=self._run_notebook)
self._thread = KThread(
target=self._run_notebook,
daemon=True,
name=f"TaipyGUI-{port}"
)
self._thread.start()
return

self._is_running = True
run_config = {
"app": self._server,
Expand All @@ -407,17 +423,44 @@ def is_running(self):
def stop_thread(self):
if hasattr(self, "_thread") and self._thread.is_alive() and self._is_running:
self._is_running = False
with contextlib.suppress(Exception):

try:
if self._get_async_mode() == "gevent":
if self._ws.wsgi_server is not None: # type: ignore[attr-defined]
self._ws.wsgi_server.stop() # type: ignore[attr-defined]
if hasattr(self._ws, 'wsgi_server') and self._ws.wsgi_server is not None:
self._ws.wsgi_server.stop()
else:
self._thread.kill()
else:
self._thread.kill()
except Exception as e:
_TaipyLogger._get_logger().warning(f"Error stopping thread: {e}")

timeout_start = time.time()
timeout_duration = 5.0 # 5 seconds timeout

while _is_port_open(self._host, self._port):
if time.time() - timeout_start > timeout_duration:
_TaipyLogger._get_logger().warning(
f"Port {self._port} still occupied after {timeout_duration}s timeout"
)
break
time.sleep(0.1)

def __del__(self):
try:
if hasattr(self, '_thread') and self._thread and self._thread.is_alive():
self.stop_thread()
if hasattr(self, '_proxy'):
self.stop_proxy()
except Exception:
pass

def stop_proxy(self):
if hasattr(self, "_proxy"):
self._proxy.stop()
try:
self._proxy.stop()
except Exception as e:
_TaipyLogger._get_logger().warning(f"Error stopping proxy: {e}")
finally:
if hasattr(self, "_proxy"):
delattr(self, "_proxy")
77 changes: 68 additions & 9 deletions taipy/gui/utils/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
# specific language governing permissions and limitations under the License.

import contextlib
import threading
import typing as t
import warnings
from threading import Thread
from threading import Event, Thread
from urllib.parse import quote as urlquote
from urllib.parse import urlparse

Expand All @@ -21,6 +22,7 @@
from twisted.web.resource import Resource
from twisted.web.server import NOT_DONE_YET, Site

from .._warnings import _warn
from .is_port_open import _is_port_open

# flake8: noqa: E402
Expand All @@ -29,10 +31,10 @@
warnings.filterwarnings(
"ignore",
category=UserWarning,
message="You do not have a working installation of the service_identity module: 'No module named 'service_identity''.*", # noqa: E501
message="You don't have a working installation of the service_identity module: "
"'No module named 'service_identity''.*",
)


if t.TYPE_CHECKING:
from ..gui import Gui

Expand Down Expand Up @@ -93,23 +95,80 @@ def __init__(self, gui: "Gui", listening_port: int) -> None:
self._listening_port = listening_port
self._gui = gui
self._is_running = False
self._thread: t.Optional[Thread] = None
self._stop_event = Event()
self._reactor_thread_id: t.Optional[int] = None

def run(self):
if self._is_running:
if self._is_running and self._thread and self._thread.is_alive():
return

host = self._gui._get_config("host", "127.0.0.1")
port = self._listening_port

if _is_port_open(host, port):
raise ConnectionError(
f"Port {port} is already opened on {host}. You have another server application running on the same port." # noqa: E501
f"Port {port} is already opened on {host}. "
f"You have another server application running on the same port."
)
site = Site(_TaipyReverseProxyResource(host, b"", self._gui))
reactor.listenTCP(port, site)
Thread(target=reactor.run, args=(False,)).start()

self._thread = Thread(
target=self._run_reactor,
args=(host, port),
daemon=True,
name=f"TaipyNotebookProxy-{port}"
)

self._stop_event.clear()
self._thread.start()
self._is_running = True

import time
time.sleep(0.1)

def _run_reactor(self, host: str, port: int):
try:
ident = threading.current_thread().ident
self._reactor_thread_id = ident if ident is not None else 0
site = Site(_TaipyReverseProxyResource(host, b"", self._gui))
reactor.listenTCP(port, site)

reactor.run(installSignalHandlers=False)

except Exception as e:
_warn(f"Reactor error: {e}")
finally:
self._is_running = False
self._reactor_thread_id = None

def stop(self):
if not self._is_running:
return

self._stop_event.set()
self._is_running = False
reactor.stop()

if (self._reactor_thread_id and
threading.current_thread().ident == self._reactor_thread_id):
reactor.stop()
else:

reactor.callFromThread(reactor.stop)

if self._thread and self._thread.is_alive():
self._thread.join(timeout=2.0)

if self._thread.is_alive():
_warn(f"Warning: Proxy thread {self._thread.name} did not terminate cleanly")

self._thread = None
self._reactor_thread_id = None

def is_alive(self) -> bool:
return (self._is_running and
self._thread is not None and
self._thread.is_alive())

def __del__(self):
with contextlib.suppress(Exception):
self.stop()
Loading