Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LOCAL_RUN=true
118 changes: 113 additions & 5 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,116 @@
import streamlit as st
import streamlit.components.v1 as components
from pathlib import Path
import json
# For some reason the windows version only works if this is imported here
import pyopenms
import os
import time
import threading
import signal
import tornado.web
Copy link
Contributor

Choose a reason for hiding this comment

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

import tornado.ioloop
import pyopenms # required import for Windows

# Keep settings at the very top
if "settings" not in st.session_state:
with open("settings.json", "r") as f:
st.session_state.settings = json.load(f)
with open("settings.json", "r") as f:
st.session_state.settings = json.load(f)

# --- Global Session Counter ---
global_active_sessions = 0
last_heartbeat = 0 # Global heartbeat timestamp
lock = threading.Lock() # Protects modifications to globals

# Add a flag to ensure Tornado starts only once.
if "tornado_server_started" not in globals():
globals()["tornado_server_started"] = False


def increment_active_sessions():
global global_active_sessions
with lock:
global_active_sessions += 1
print(f"Session connected, total active sessions: {global_active_sessions}")

def decrement_active_sessions():
global global_active_sessions
with lock:
global_active_sessions -= 1
print(f"Session disconnected, total active sessions: {global_active_sessions}")
if global_active_sessions <= 0:
shutdown()

def shutdown():
print("🚨 No active users. Shutting down Streamlit server...")
os.kill(os.getpid(), signal.SIGTERM)

# Register this session only once.
if "registered" not in st.session_state:
st.session_state.registered = True
increment_active_sessions()

# --- Tornado Endpoints ---

# This handler is called on browser unload (if it fires)
class CloseAppHandler(tornado.web.RequestHandler):
def post(self):
time.sleep(1)
decrement_active_sessions()
self.write("OK")

Comment on lines +53 to +59
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add client-side usage of the /_closeapp endpoint.
The CloseAppHandler endpoint is defined but never invoked by the client script. Implementing a beforeunload or unload listener with navigator.sendBeacon('/_closeapp') ensures the server receives disconnection signals reliably.

 <script>
   function sendHeartbeat() {
       fetch('http://localhost:8502/heartbeat', {method: 'POST'});
   }
   // ...
+  window.addEventListener('beforeunload', () => {
+      navigator.sendBeacon('http://localhost:8502/_closeapp');
+  });
 </script>

Also applies to: 70-71

# This handler receives heartbeat pings
class HeartbeatHandler(tornado.web.RequestHandler):
def post(self):
global last_heartbeat
with lock:
last_heartbeat = time.time()
self.write("OK")

def start_tornado_server():
routes = [
(r"/_closeapp", CloseAppHandler),
(r"/heartbeat", HeartbeatHandler)
]
app = tornado.web.Application(routes)
app.listen(port=8502) # Adjust if needed; matches Streamlit port.
tornado.ioloop.IOLoop.current().start()

threading.Thread(target=start_tornado_server, daemon=True).start()

# Monitor for lost heartbeat and shutdown if no activity.
def heartbeat_monitor():
global last_heartbeat
# Initialize heartbeat time
with lock:
last_heartbeat = time.time()
while True:
time.sleep(5)
# If no heartbeat received in 15 seconds and no sessions are active, shutdown
current_time = time.time()
with lock:
elapsed = current_time - last_heartbeat
active = global_active_sessions
if elapsed > 15:
shutdown()

threading.Thread(target=heartbeat_monitor, daemon=True).start()

# --- Client-side JavaScript injections ---
def insert_heartbeat_script():
# Sends a heartbeat every 3 seconds.
components.html(
"""
<script>
function sendHeartbeat() {
fetch('http://localhost:8502/heartbeat', {method: 'POST'});
}
// Send an immediate heartbeat on load
sendHeartbeat();
// Then every 3 seconds
setInterval(sendHeartbeat, 3000);
</script>
""",
height=0,
)

if __name__ == '__main__':
pages = {
Expand All @@ -33,4 +137,8 @@
}

pg = st.navigation(pages)
pg.run()
pg.run()

if os.getenv("LOCAL_RUN", "true").lower() == "true":
# Inject the heartbeat script first so the server sees regular pings.
insert_heartbeat_script()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ plotly==5.22.0
captcha==0.5.0
pyopenms_viz==1.0.0
streamlit-js-eval
tornado>=6.2.0
Loading