mirror of https://github.com/DJ2LS/FreeDATA.git
298 lines
11 KiB
Python
298 lines
11 KiB
Python
|
|
import os
|
|
import sys
|
|
# we need to add script directory to the sys path for avoiding problems with pip package
|
|
script_directory = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.append(script_directory)
|
|
|
|
import signal
|
|
import queue
|
|
import asyncio
|
|
import webbrowser
|
|
from fastapi import FastAPI, Request, WebSocket
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
from contextlib import asynccontextmanager
|
|
import uvicorn
|
|
import threading
|
|
from config import CONFIG
|
|
import audio
|
|
import service_manager
|
|
import state_manager
|
|
import websocket_manager
|
|
import event_manager
|
|
import structlog
|
|
|
|
|
|
from message_system_db_manager import DatabaseManager
|
|
from message_system_db_attachments import DatabaseManagerAttachments
|
|
from schedule_manager import ScheduleManager
|
|
|
|
from api.general import router as general_router
|
|
from api.config import router as config_router
|
|
from api.devices import router as devices_router
|
|
from api.radio import router as radio_router
|
|
from api.modem import router as modem_router
|
|
from api.freedata import router as freedata_router
|
|
from api.websocket import router as websocket_router
|
|
from constants import CONFIG_ENV_VAR, DEFAULT_CONFIG_FILE, MODEM_VERSION, API_VERSION, LICENSE, DOCUMENTATION_URL
|
|
|
|
# adjust asyncio for windows usage for avoiding a Assertion Error
|
|
if sys.platform == 'win32':
|
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
#print("startup")
|
|
yield
|
|
stop_server()
|
|
|
|
app = FastAPI(lifespan=lifespan)
|
|
app.include_router(general_router, prefix="")
|
|
app.include_router(config_router, prefix="/config")
|
|
app.include_router(devices_router, prefix="/devices")
|
|
app.include_router(radio_router, prefix="/radio")
|
|
app.include_router(modem_router, prefix="/modem")
|
|
app.include_router(freedata_router, prefix="/freedata")
|
|
app.include_router(websocket_router, prefix="")
|
|
# custom logger for fastapi
|
|
#setup_logging()
|
|
logger = structlog.get_logger()
|
|
|
|
potential_gui_dirs = [
|
|
"../freedata_gui/dist", # running server with "python3 server.py
|
|
"freedata_gui/dist", # running sever with ./tools/run-server.py
|
|
"FreeDATA/freedata_gui/dist", # running server with bash run-server...
|
|
os.path.join(os.path.dirname(__file__), "gui") # running server as nuitka bundle
|
|
]
|
|
|
|
# Check which directory exists and set gui_dir accordingly
|
|
gui_dir = None
|
|
for dir_path in potential_gui_dirs:
|
|
if os.path.isdir(dir_path):
|
|
gui_dir = dir_path
|
|
break
|
|
|
|
# Configure app to serve static files if gui_dir is found
|
|
if gui_dir:
|
|
app.mount("/gui", StaticFiles(directory=gui_dir, html=True), name="static")
|
|
else:
|
|
logger.warning("GUI directory not found. Please run `npm i && npm run build` inside `freedata_gui`.")
|
|
|
|
|
|
|
|
@app.middleware("http")
|
|
async def http_middleware(request: Request, call_next):
|
|
"""Middleware function for HTTP requests.
|
|
|
|
This middleware function intercepts HTTP requests, disables caching,
|
|
logs request details (method, URL, and response code), and passes the
|
|
request to the next middleware or route handler. It also includes
|
|
commented-out code for enabling caching, which can be activated if
|
|
needed.
|
|
|
|
Args:
|
|
request (Request): The incoming HTTP request.
|
|
call_next: The next middleware function or route handler in the chain.
|
|
|
|
Returns:
|
|
Response: The HTTP response.
|
|
"""
|
|
response = await call_next(request)
|
|
|
|
# Disable caching
|
|
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, private"
|
|
response.headers["Pragma"] = "no-cache" # For HTTP/1.0 backward compatibility
|
|
|
|
# Enable caching for 1 day
|
|
# response.headers["Cache-Control"] = "public, max-age=86400" # Cache for 86400 seconds (1 day)
|
|
# response.headers["Pragma"] = "cache" # backward compatibility with HTTP/1.0
|
|
# response.headers["Expires"] = "0" # Forces modern clients to use max-age
|
|
|
|
# Log requests
|
|
logger.info(f"[API] {request.method}", url=str(request.url), response_code=response.status_code)
|
|
return response
|
|
|
|
|
|
# CORS
|
|
origins = ["*"]
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
|
|
|
|
# Set config file to use
|
|
def set_config():
|
|
"""Sets the configuration file to use.
|
|
|
|
This function retrieves the path to the configuration file from the
|
|
environment variable CONFIG_ENV_VAR or defaults to DEFAULT_CONFIG_FILE
|
|
in the script directory. It checks if the file exists and exits the
|
|
program if not found.
|
|
|
|
Returns:
|
|
str: The path to the configuration file.
|
|
"""
|
|
config_file = os.getenv(CONFIG_ENV_VAR, os.path.join(script_directory, DEFAULT_CONFIG_FILE))
|
|
if os.path.exists(config_file):
|
|
print(f"Using config from {config_file}")
|
|
else:
|
|
print(f"Config file '{config_file}' not found. Exiting.")
|
|
sys.exit(1)
|
|
return config_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Signal Handler
|
|
def signal_handler(sig, frame):
|
|
"""Handles SIGINT signal (Ctrl+C).
|
|
|
|
This function is called when the user interrupts the server with
|
|
Ctrl+C (SIGINT signal). It logs a warning message and initiates the
|
|
server shutdown process. The frame argument is not currently used.
|
|
|
|
Args:
|
|
sig: The signal number (SIGINT).
|
|
frame: The current stack frame.
|
|
"""
|
|
print("\n------------------------------------------")
|
|
logger.warning("[SHUTDOWN] Received SIGINT....")
|
|
stop_server()
|
|
|
|
def stop_server():
|
|
"""Stops the FreeDATA server and its components.
|
|
|
|
This function performs a graceful shutdown of the FreeDATA server,
|
|
including stopping transmissions, shutting down various managers and
|
|
services, closing socket interfaces, and terminating audio streams.
|
|
It attempts to handle potential errors during the shutdown process.
|
|
"""
|
|
# INFO attempt stopping ongoing transmission for reducing chance of stuck PTT
|
|
if hasattr(app, 'state_manager'):
|
|
logger.warning("[SHUTDOWN] Stopping ongoing transmissions....")
|
|
try:
|
|
for session in app.state_manager.arq_irs_sessions.values():
|
|
#session.abort_transmission()
|
|
session.transmission_aborted()
|
|
for session in app.state_manager.arq_iss_sessions.values():
|
|
session.abort_transmission(send_stop=False)
|
|
session.transmission_aborted()
|
|
except Exception as e:
|
|
print(f"Error during transmission stopping: {e}")
|
|
|
|
if hasattr(app, 'wsm'):
|
|
app.wsm.shutdown()
|
|
if hasattr(app, 'radio_manager'):
|
|
app.radio_manager.stop()
|
|
if hasattr(app, 'schedule_manager'):
|
|
app.schedule_manager.stop()
|
|
if hasattr(app.service_manager, 'modem_service') and app.service_manager.modem_service:
|
|
app.service_manager.shutdown()
|
|
if hasattr(app.service_manager, 'modem') and app.service_manager.modem:
|
|
app.service_manager.modem.demodulator.shutdown()
|
|
if hasattr(app.service_manager, 'modem_service'):
|
|
app.service_manager.stop_modem()
|
|
if hasattr(app, 'socket_interface_manager') and app.socket_interface_manager:
|
|
app.socket_interface_manager.stop_servers()
|
|
if hasattr(app, 'socket_interface_manager') and app.socket_interface_manager:
|
|
app.socket_interface_manager.stop_servers()
|
|
audio.terminate()
|
|
logger.warning("[SHUTDOWN] Shutdown completed")
|
|
try:
|
|
# it seems sys.exit causes problems since we are using fastapi
|
|
# fastapi seems to close the application
|
|
#sys.exit(0)
|
|
os._exit(0)
|
|
|
|
pass
|
|
except Exception as e:
|
|
logger.warning("[SHUTDOWN] Shutdown completed", error=e)
|
|
|
|
|
|
def open_browser_after_delay(url, delay=2):
|
|
"""Opens the specified URL in a web browser after a delay.
|
|
|
|
This function opens the given URL in a new browser window or tab after
|
|
a specified delay. It is used to automatically open the FreeDATA GUI
|
|
in a browser after the server has started.
|
|
|
|
Args:
|
|
url (str): The URL to open.
|
|
delay (int, optional): The delay in seconds before opening the browser. Defaults to 2.
|
|
"""
|
|
threading.Event().wait(delay)
|
|
webbrowser.open(url, new=0, autoraise=True)
|
|
|
|
def main():
|
|
"""Main function to start the FreeDATA server.
|
|
|
|
This function initializes the FreeDATA server, sets up signal
|
|
handling, configures managers and services, starts the modem,
|
|
initializes the database, starts websocket workers, and runs the
|
|
FastAPI server using uvicorn. It also handles automatic opening of
|
|
the GUI in a web browser.
|
|
"""
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
app.MODEM_VERSION = MODEM_VERSION
|
|
app.API_VERSION = API_VERSION
|
|
|
|
config_file = set_config()
|
|
app.config_manager = CONFIG(config_file)
|
|
app.p2p_data_queue = queue.Queue()
|
|
app.state_queue = queue.Queue()
|
|
app.modem_events = queue.Queue()
|
|
app.modem_fft = queue.Queue()
|
|
app.modem_service = queue.Queue()
|
|
app.event_manager = event_manager.EventManager([app.modem_events])
|
|
app.state_manager = state_manager.StateManager(app.state_queue)
|
|
app.schedule_manager = ScheduleManager(app.MODEM_VERSION, app.config_manager, app.state_manager, app.event_manager)
|
|
app.service_manager = service_manager.SM(app)
|
|
app.modem_service.put("start")
|
|
DatabaseManager(app.event_manager).check_database_version()
|
|
DatabaseManager(app.event_manager).initialize_default_values()
|
|
DatabaseManager(app.event_manager).database_repair_and_cleanup()
|
|
DatabaseManagerAttachments(app.event_manager).clean_orphaned_attachments()
|
|
|
|
app.wsm = websocket_manager.wsm()
|
|
app.wsm.startWorkerThreads(app)
|
|
|
|
conf = app.config_manager.read()
|
|
modemaddress = conf['NETWORK'].get('modemaddress', '127.0.0.1')
|
|
modemport = int(conf['NETWORK'].get('modemport', 5000))
|
|
|
|
# check if modemadress is empty - known bug caused by older versions
|
|
if modemaddress in ['', None]:
|
|
modemaddress = '127.0.0.1'
|
|
|
|
if modemport in ['', None]:
|
|
modemport = 5000 # Use integer value
|
|
|
|
if gui_dir and os.path.isdir(gui_dir):
|
|
url = f"http://{modemaddress}:{modemport}/gui"
|
|
|
|
logger.info("---------------------------------------------------")
|
|
logger.info(" ")
|
|
logger.info(f"[GUI] AVAILABLE ON {url}")
|
|
logger.info("just open it in your browser")
|
|
logger.info(" ")
|
|
logger.info("---------------------------------------------------")
|
|
|
|
if conf['GUI'].get('auto_run_browser', True):
|
|
threading.Thread(target=open_browser_after_delay, args=(url, 2)).start()
|
|
|
|
uvicorn.run(app, host=modemaddress, port=modemport, log_config=None, log_level="info")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|