Skip to content

Commit

Permalink
Merge branch 'hugh/fix-apps' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewWilkes committed May 31, 2024
2 parents bc5bb28 + 8e209e5 commit 0f3c8aa
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 51 deletions.
156 changes: 105 additions & 51 deletions modules/firmware_apps/app_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io
import json
import os
import tarfile
from tarfile import DIRTYPE, TarFile
from typing import Any, Callable

Expand All @@ -12,7 +13,23 @@
from events.input import BUTTON_TYPES, ButtonDownEvent
from requests import get
from system.eventbus import eventbus
from system.launcher.app import APP_DIR, list_user_apps
from system.launcher.app import APP_DIR, list_user_apps, InstallNotificationEvent
from system.notification.events import ShowNotificationEvent


def dir_exists(filename):
try:
return (os.stat(filename)[0] & 0x4000) != 0
except OSError:
return False


def file_exists(filename):
try:
return (os.stat(filename)[0] & 0x4000) == 0
except OSError:
return False


APP_STORE_LISTING_LIVE_URL = "https://api.badge.emf.camp/v1/apps"
APP_STORE_LISTING_URL = "https://apps.badge.emfcamp.org/demo_api/apps.json"
Expand Down Expand Up @@ -53,20 +70,10 @@ def cleanup_ui_widgets(self):
widget._cleanup()
widget = None

def check_wifi(self):
self.update_state("checking_wifi")
connect_wifi()

if self.state != "checking_wifi":
self.update_state("checking_wifi")
connected = wifi.status()
if not connected:
self.update_state("no_wifi")
return connected

def get_index(self):
if not self.check_wifi():
if not wifi.status():
self.update_state("no_wifi")
return
self.update_state("refreshing_index")

def background_update(self, delta):
Expand Down Expand Up @@ -95,12 +102,13 @@ def install_app(self, app):
try:
install_app(app)
self.update_state("main_menu")
# TODO Notify success
eventbus.emit(InstallNotificationEvent())
eventbus.emit(ShowNotificationEvent("Installed the app!"))
except MemoryError:
self.update_state("install_oom")
except Exception as e:
print(e)
# TODO Notify user of failure
eventbus.emit(ShowNotificationEvent("Couldn't install app"))
self.update_state("main_menu")

def update_state(self, state):
Expand All @@ -121,6 +129,8 @@ def prepare_available_menu(self):
def on_select(_, i):
self.to_install_app = self.app_store_index[i]
self.update_state("installing_app")
if self.available_menu:
self.available_menu._cleanup()

def exit_available_menu():
self.cleanup_ui_widgets()
Expand Down Expand Up @@ -201,8 +211,20 @@ def error_screen(self, ctx, message):

def update(self, delta):
if self.state == "init":
if not wifi.status():
self.update_state("wifi_init")
return
print("calling get index")
self.get_index()
elif self.state == "wifi_init":
try:
wifi.connect()
except Exception:
pass
self.update_state("wifi_connecting")
elif self.state == "wifi_connecting":
if wifi.status():
self.update_state("init")
elif self.state == "index_received":
self.handle_index()
elif self.state == "main_menu" and not self.menu:
Expand Down Expand Up @@ -240,6 +262,8 @@ def draw(self, ctx):
self.error_screen(ctx, "Couldn't\nconnect to\napp store")
elif self.state == "checking_wifi":
self.error_screen(ctx, "Checking\nWi-Fi connection")
elif self.state in ("wifi_init", "wifi_connecting"):
self.error_screen(ctx, "Connecting\nWi-Fi...\n")
elif self.state == "refreshing_index":
self.error_screen(ctx, "Refreshing\napp store\nindex")
elif self.state == "install_oom":
Expand Down Expand Up @@ -304,30 +328,54 @@ def draw(self, ctx):
def install_app(app):
try:
## This is fine to block because we only call it from background_update
print("GC")
gc.collect()

print(f"Getting {app["tarballUrl"]}")
tarball = get(app["tarballUrl"])
# tarballGenerator = self.download_file(app["tarballUrl"])

# TODO: Investigate using deflate.DeflateIO instead. Can't do it now
# because it's not available in the simulator.
print("Decompressing")
tar = gzip.decompress(tarball.content)
gc.collect()
t = TarFile(fileobj=io.BytesIO(tar))
tar_bytesio = io.BytesIO(tar)

prefix = validate_app_files(t)
print("Validating")
prefix = find_app_root_dir(TarFile(fileobj=tar_bytesio))
tar_bytesio.seek(0)
app_py_info = find_app_py_file(prefix, TarFile(fileobj=tar_bytesio))
print(f"Found app.py at: {app_py_info.name}")
tar_bytesio.seek(0)

# TODO: Check we have enough storage in advance
# TODO: Does the app already exist? Delete it

# Make sure apps dir exists
try:
os.mkdir(APP_DIR)
except OSError:
pass

t = TarFile(fileobj=tar_bytesio)
for i in t:
if i:
if not i.name.startswith(prefix):
continue
if i.type == DIRTYPE:
dirname = os.path.join(APP_DIR, i.name)
if not os.path.exists(dirname):
os.makedirs(dirname)
dirname = f"{APP_DIR}/{i.name}"
print(f"Dirname: {dirname}")
if not dir_exists(dirname):
try:
print(f"Creating {dirname}")
os.mkdir(dirname.rstrip("/"))
except OSError:
print(f"Failed to create {dirname}")
pass
else:
filename = os.path.join(APP_DIR, i.name)
filename = f"{APP_DIR}/{i.name}"
print(f"Filename: {filename}")
f = t.extractfile(i)
if f:
with open(filename, "wb") as file:
Expand All @@ -338,9 +386,9 @@ def install_app(app):
"name": app["manifest"]["app"]["name"],
"hidden": False,
}
with open(
os.path.join(APP_DIR, prefix, "app_data.json"), "w+"
) as internal_manifest_file_handler:
json_path = f"{APP_DIR}/{prefix}/metadata.json"
print(f"Json path: {json_path}")
with open(json_path, "w+") as internal_manifest_file_handler:
json.dump(internal_manifest, internal_manifest_file_handler)

except MemoryError as e:
Expand All @@ -352,35 +400,41 @@ def install_app(app):


def validate_app_files(tar):
prefix = None
seen_app_py = False
for i, f in enumerate(tar):
print(f.name)
if i == 0 and f.isdir():
prefix = f.name
continue
if prefix and not f.name.startswith(prefix):
raise ValueError(f"Invalid file {f.name}")
if not seen_app_py and prefix and f.name == prefix + "/app.py":
seen_app_py = True
if not prefix:
raise ValueError("No root dir in tarball")
if not seen_app_py:
raise ValueError("No app.py found in tarball")
prefix = find_app_root_dir(tar)
app_py_path = find_app_py_file(prefix, tar)
print(f"Found app.py at: {app_py_path}")
return prefix


def connect_wifi():
ssid = wifi.get_ssid()
if not ssid:
print("No WIFI config!")
return
def find_app_root_dir(tar):
print("Finding root dir...")
root_dir = None
for i, f in enumerate(tar):
print(f"prefix: {i}, name: {f.name}")
slash_count = len(f.name.split("/")) - 1
if slash_count == 1 and f.isdir():
if root_dir is None:
root_dir = f.name
else:
raise ValueError("More than one root directory found in app tarball")
if root_dir is None:
raise ValueError("No root dir in tarball")
return root_dir

if not wifi.status():
wifi.connect()
while True:
print("Connecting to")
print(f"{ssid}...")

# if wifi.wait():
# break
def find_app_py_file(prefix, tar) -> tarfile.TarInfo:
print("Finding app.py...")
found_app_py = False
expected_path = f"{prefix}app.py"
app_py_info = None

for i, f in enumerate(tar):
print(f"prefix: {i}, name: {f.name}")
if f.name == expected_path:
found_app_py = True
app_py_info = f
if not found_app_py:
raise ValueError(
f"No app.py found in tarball, expected location: {expected_path}"
)
return app_py_info
6 changes: 6 additions & 0 deletions modules/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# main.py -- put your code here!
from esp32 import Partition
import wifi

from system.scheduler import scheduler
from system.hexpansion.app import HexpansionManagerApp
Expand All @@ -26,6 +27,11 @@
# Start notification handler
scheduler.start_app(NotificationService(), always_on_top=True)

try:
wifi.connect()
except Exception:
pass

PowerEventHandler.RegisterDefaultCallbacks(PowerEventHandler)

Partition.mark_app_valid_cancel_rollback()
Expand Down
7 changes: 7 additions & 0 deletions modules/system/launcher/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from app_components.menu import Menu
from perf_timer import PerfTimer
from system.eventbus import eventbus
from events import Event
from system.scheduler.events import (
RequestForegroundPushEvent,
RequestStartAppEvent,
Expand All @@ -14,6 +15,8 @@

APP_DIR = "/apps"

class InstallNotificationEvent(Event):
pass

def path_isfile(path):
# Wow totally an elegant way to do os.path.isfile...
Expand Down Expand Up @@ -83,6 +86,10 @@ def __init__(self):
self.update_menu()
self._apps = {}
eventbus.on_async(RequestStopAppEvent, self._handle_stop_app, self)
eventbus.on_async(InstallNotificationEvent, self._handle_refresh_notifications, self)

async def _handle_refresh_notifications(self, _):
self.update_menu()

async def _handle_stop_app(self, event: RequestStopAppEvent):
# If an app is stopped, remove our cache of it as it needs restarting
Expand Down

0 comments on commit 0f3c8aa

Please sign in to comment.