Skip to content
Closed
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
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()
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ plotly==5.22.0
captcha==0.5.0
pyopenms_viz==1.0.0
streamlit-js-eval
psutil==7.0.0
tornado>=6.2.0
psutil==7.0.0