mirror of https://github.com/LedFx/LedFx.git
415 lines
12 KiB
Python
415 lines
12 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
Entry point for the ledfx controller. To run this script for development
|
|
purposes use:
|
|
|
|
[console_scripts]
|
|
python setup.py develop
|
|
ledfx
|
|
|
|
For non-development purposes run:
|
|
|
|
[console_scripts]
|
|
python setup.py install
|
|
ledfx
|
|
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from logging.handlers import RotatingFileHandler
|
|
|
|
import psutil
|
|
import yappi
|
|
from pyupdater.client import Client
|
|
|
|
import ledfx.config as config_helpers
|
|
from ledfx.consts import (
|
|
PROJECT_NAME,
|
|
PROJECT_VERSION,
|
|
REQUIRED_PYTHON_STRING,
|
|
REQUIRED_PYTHON_VERSION,
|
|
)
|
|
from ledfx.core import LedFxCore
|
|
from ledfx.utils import currently_frozen
|
|
|
|
# Logger Variables
|
|
PYUPDATERLOGLEVEL = 35
|
|
|
|
|
|
def validate_python() -> None:
|
|
"""Validate the python version for when manually running"""
|
|
|
|
if sys.version_info[:3] < REQUIRED_PYTHON_VERSION:
|
|
print(("Python {} is required.").format(REQUIRED_PYTHON_STRING))
|
|
sys.exit(1)
|
|
|
|
|
|
def reset_logging():
|
|
manager = logging.root.manager
|
|
manager.disabled = logging.NOTSET
|
|
for logger in manager.loggerDict.values():
|
|
if isinstance(logger, logging.Logger):
|
|
logger.setLevel(logging.NOTSET)
|
|
logger.propagate = True
|
|
logger.disabled = False
|
|
logger.filters.clear()
|
|
handlers = logger.handlers.copy()
|
|
for handler in handlers:
|
|
# Copied from `logging.shutdown`.
|
|
try:
|
|
handler.acquire()
|
|
handler.flush()
|
|
handler.close()
|
|
except (OSError, ValueError):
|
|
pass
|
|
finally:
|
|
handler.release()
|
|
logger.removeHandler(handler)
|
|
|
|
|
|
def setup_logging(loglevel, config_dir):
|
|
# Create a custom logging level to virtual pyupdater progress
|
|
reset_logging()
|
|
|
|
console_loglevel = loglevel or logging.WARNING
|
|
console_logformat = "[%(levelname)-8s] %(name)-30s : %(message)s"
|
|
|
|
file_loglevel = logging.INFO
|
|
file_logformat = "%(asctime)-8s %(name)-30s %(levelname)-8s %(message)s"
|
|
|
|
root_logger = logging.getLogger()
|
|
|
|
file_handler = RotatingFileHandler(
|
|
config_helpers.get_log_file_location(config_dir),
|
|
mode="a", # append
|
|
maxBytes=0.5 * 1000 * 1000, # 512kB
|
|
encoding="utf8",
|
|
backupCount=5, # once it hits 2.5MB total, start removing logs.
|
|
)
|
|
file_handler.setLevel(file_loglevel) # set loglevel
|
|
file_formatter = logging.Formatter(
|
|
file_logformat
|
|
) # a simple log file format
|
|
file_handler.setFormatter(
|
|
file_formatter
|
|
) # tell the file_handler to use this format
|
|
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.setLevel(console_loglevel) # set loglevel
|
|
console_formatter = logging.Formatter(
|
|
console_logformat
|
|
) # a simple console format
|
|
console_handler.setFormatter(
|
|
console_formatter
|
|
) # tell the console_handler to use this format
|
|
|
|
# add the handlers to the root logger
|
|
root_logger.setLevel(logging.DEBUG)
|
|
root_logger.addHandler(console_handler)
|
|
root_logger.addHandler(file_handler)
|
|
|
|
logging.addLevelName(PYUPDATERLOGLEVEL, "Updater")
|
|
|
|
# Suppress some of the overly verbose logs
|
|
logging.getLogger("sacn").setLevel(logging.WARNING)
|
|
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
|
logging.getLogger("pyupdater").setLevel(logging.WARNING)
|
|
logging.getLogger("zeroconf").setLevel(logging.WARNING)
|
|
|
|
global _LOGGER
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
description="A Networked LED Effect Controller"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--version",
|
|
action="version",
|
|
version=f"ledfx {PROJECT_VERSION}",
|
|
)
|
|
parser.add_argument(
|
|
"-c",
|
|
"--config",
|
|
dest="config",
|
|
help="Directory that contains the configuration files",
|
|
default=config_helpers.get_default_config_directory(),
|
|
type=str,
|
|
)
|
|
parser.add_argument(
|
|
"--open-ui",
|
|
dest="open_ui",
|
|
action="store_true",
|
|
help="Automatically open the webinterface",
|
|
)
|
|
parser.add_argument(
|
|
"-v",
|
|
"--verbose",
|
|
dest="loglevel",
|
|
help="set loglevel to INFO",
|
|
action="store_const",
|
|
const=logging.INFO,
|
|
)
|
|
parser.add_argument(
|
|
"-vv",
|
|
"--very-verbose",
|
|
dest="loglevel",
|
|
help="set loglevel to DEBUG",
|
|
action="store_const",
|
|
const=logging.DEBUG,
|
|
)
|
|
parser.add_argument(
|
|
"-p",
|
|
"--port",
|
|
dest="port",
|
|
help="Web interface port (HTTP)",
|
|
default=None,
|
|
type=int,
|
|
)
|
|
parser.add_argument(
|
|
"-p_s",
|
|
"--port_secure",
|
|
dest="port_s",
|
|
help="Web interface port (HTTPS)",
|
|
default=None,
|
|
type=int,
|
|
)
|
|
parser.add_argument(
|
|
"--host",
|
|
dest="host",
|
|
help="The address to host LedFx web interface",
|
|
default=None,
|
|
type=str,
|
|
)
|
|
parser.add_argument(
|
|
"--tray",
|
|
dest="tray",
|
|
action="store_true",
|
|
help="Hide LedFx console to the system tray",
|
|
)
|
|
parser.add_argument(
|
|
"--performance",
|
|
dest="performance",
|
|
action="store_true",
|
|
help="Profile LedFx's performance. A developer can use this to diagnose performance issues.",
|
|
)
|
|
parser.add_argument(
|
|
"--offline",
|
|
dest="offline_mode",
|
|
action="store_true",
|
|
help="Disable automated updates and sentry crash logger",
|
|
)
|
|
parser.add_argument(
|
|
"--sentry-crash-test",
|
|
dest="sentry_test",
|
|
action="store_true",
|
|
help="This crashes LedFx to test the sentry crash logger",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def installed_via_pip():
|
|
|
|
"""Check to see if LedFx is installed via pip
|
|
Returns:
|
|
boolean
|
|
"""
|
|
pip_package_command = subprocess.check_output(
|
|
[sys.executable, "-m", "pip", "freeze"]
|
|
)
|
|
installed_packages = [
|
|
r.decode().split("==")[0] for r in pip_package_command.split()
|
|
]
|
|
if "ledfx" in installed_packages:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def update_ledfx(icon=None):
|
|
# initialize & refresh in one update, check client
|
|
|
|
def notify(msg):
|
|
if icon and icon.HAS_NOTIFICATION:
|
|
icon.remove_notification()
|
|
icon.notify(msg)
|
|
_LOGGER.log(PYUPDATERLOGLEVEL, msg)
|
|
|
|
def log_status_info(info):
|
|
total = info.get("total")
|
|
downloaded = info.get("downloaded")
|
|
percent_complete = info.get("percent_complete")
|
|
time = info.get("time")
|
|
_LOGGER.log(
|
|
PYUPDATERLOGLEVEL,
|
|
f"{downloaded} of {total} [{percent_complete} complete, {time} remaining]",
|
|
)
|
|
|
|
class ClientConfig:
|
|
PUBLIC_KEY = "Txce3TE9BUixsBtqzDba6V5vBYltt/0pw5oKL8ueCDg"
|
|
APP_NAME = PROJECT_NAME
|
|
COMPANY_NAME = "LedFx Developers"
|
|
HTTP_TIMEOUT = 5
|
|
MAX_DOWNLOAD_RETRIES = 2
|
|
UPDATE_URLS = ["https://ledfx.app/downloads/"]
|
|
|
|
client = Client(ClientConfig(), refresh=True)
|
|
_LOGGER.log(PYUPDATERLOGLEVEL, "Checking for updates...")
|
|
# First we check for updates.
|
|
# If an update is found, an update object will be returned
|
|
# If no updates are available, None will be returned
|
|
ledfx_update = client.update_check(PROJECT_NAME, PROJECT_VERSION)
|
|
|
|
# Download the update
|
|
if ledfx_update is not None:
|
|
client.add_progress_hook(log_status_info)
|
|
_LOGGER.log(PYUPDATERLOGLEVEL, "Update found!")
|
|
notify(
|
|
"Downloading update, please wait... LedFx will restart when complete."
|
|
)
|
|
ledfx_update.download()
|
|
# Install and restart
|
|
if ledfx_update.is_downloaded():
|
|
notify("Download complete. Restarting LedFx...")
|
|
ledfx_update.extract_restart()
|
|
else:
|
|
notify("Unable to download update.")
|
|
else:
|
|
# No Updates, into main we go
|
|
_LOGGER.log(
|
|
PYUPDATERLOGLEVEL,
|
|
"You're all up to date, enjoy the light show!",
|
|
)
|
|
|
|
|
|
def main():
|
|
"""Main entry point allowing external calls"""
|
|
args = parse_args()
|
|
config_helpers.ensure_config_directory(args.config)
|
|
setup_logging(args.loglevel, config_dir=args.config)
|
|
config_helpers.load_logger()
|
|
|
|
# Set some process priority optimisations
|
|
p = psutil.Process(os.getpid())
|
|
|
|
if psutil.WINDOWS:
|
|
try:
|
|
p.nice(psutil.HIGH_PRIORITY_CLASS)
|
|
except psutil.Error:
|
|
_LOGGER.info(
|
|
"Unable to set priority, please run as Administrator if you are experiencing frame rate issues"
|
|
)
|
|
# p.ionice(psutil.IOPRIO_HIGH)
|
|
elif psutil.LINUX:
|
|
try:
|
|
p.nice(15)
|
|
p.ionice(psutil.IOPRIO_CLASS_RT, value=7)
|
|
except psutil.Error:
|
|
_LOGGER.info(
|
|
"Unable to set priority, please run as root or sudo if you are experiencing frame rate issues",
|
|
)
|
|
else:
|
|
p.nice(15)
|
|
|
|
if not (currently_frozen() or installed_via_pip()):
|
|
if args.offline_mode:
|
|
_LOGGER.warning(
|
|
"Offline Mode Enabled - Please check for updates regularly."
|
|
)
|
|
else:
|
|
import ledfx.sentry_config # noqa: F401
|
|
|
|
if args.sentry_test:
|
|
"""This will crash LedFx and submit a Sentry error if Sentry is configured"""
|
|
_LOGGER.warning("Steering LedFx into a brick wall")
|
|
div_by_zero = 1 / 0
|
|
|
|
if args.tray or currently_frozen():
|
|
# If pystray is imported on a device that can't display it, it explodes. Catch it
|
|
try:
|
|
import pystray
|
|
except Exception as Error:
|
|
msg = f"Error: Unable to virtual tray icon. Shutting down. Error: {Error}"
|
|
_LOGGER.critical(msg)
|
|
raise Exception(msg)
|
|
sys.exit(0)
|
|
|
|
from PIL import Image
|
|
|
|
if currently_frozen():
|
|
current_directory = os.path.dirname(__file__)
|
|
icon_location = os.path.join(current_directory, "tray.png")
|
|
else:
|
|
current_directory = os.path.dirname(__file__)
|
|
icon_location = os.path.join(
|
|
current_directory, "..", "icons/" "tray.png"
|
|
)
|
|
icon = pystray.Icon(
|
|
"LedFx", icon=Image.open(icon_location), title="LedFx"
|
|
)
|
|
icon.visible = True
|
|
else:
|
|
icon = None
|
|
# icon = None
|
|
|
|
|
|
# if not args.offline_mode and currently_frozen():
|
|
# update_ledfx(icon)
|
|
|
|
if icon:
|
|
icon.run(setup=entry_point)
|
|
else:
|
|
entry_point()
|
|
|
|
|
|
def entry_point(icon=None):
|
|
# have to re-parse args here :/ no way to pass them through pysicon's setup
|
|
args = parse_args()
|
|
|
|
exit_code = 4
|
|
while exit_code == 4:
|
|
_LOGGER.info("LedFx Core is initializing")
|
|
|
|
if args.performance:
|
|
print("Collecting performance data...")
|
|
yappi.start()
|
|
|
|
ledfx = LedFxCore(
|
|
config_dir=args.config,
|
|
host=args.host,
|
|
port=args.port,
|
|
port_s=args.port_s,
|
|
icon=icon,
|
|
)
|
|
|
|
exit_code = ledfx.start(open_ui=args.open_ui)
|
|
|
|
if args.performance:
|
|
print("Finished collecting performance data")
|
|
filename = config_helpers.get_profile_dump_location(
|
|
config_dir=args.config
|
|
)
|
|
yappi.stop()
|
|
stats = yappi.get_func_stats()
|
|
yappi.get_thread_stats().print_all()
|
|
stats.save(filename, type="pstat")
|
|
print(
|
|
f"Saved performance data to config directory : {filename}"
|
|
)
|
|
print(
|
|
"Please send the performance data to a developer : https://ledfx.app/contact/"
|
|
)
|
|
|
|
if icon:
|
|
icon.stop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|