Unify neos/agnos updaters (#22109)
* start moving neos updater * downloading * ui * move recovery * resuming * add verification * fix up launch * test * update updater * fix mypy * fake updater * review suggestions * more tests * abc * update bin * raise Co-authored-by: Comma Device <device@comma.ai>pull/22196/head
parent
d5e6dd3d5b
commit
b3705ede5e
22 changed files with 324 additions and 1292 deletions
@ -1,3 +0,0 @@ |
|||||||
local/ |
|
||||||
prod/ |
|
||||||
staging/ |
|
@ -1,99 +0,0 @@ |
|||||||
CC = clang
|
|
||||||
CXX = clang++
|
|
||||||
|
|
||||||
PHONELIBS = ../../phonelibs
|
|
||||||
|
|
||||||
WARN_FLAGS = -Werror=implicit-function-declaration \
|
|
||||||
-Werror=incompatible-pointer-types \
|
|
||||||
-Werror=int-conversion \
|
|
||||||
-Werror=return-type \
|
|
||||||
-Werror=format-extra-args
|
|
||||||
|
|
||||||
CFLAGS = -std=gnu11 -g -fPIC -O2 $(WARN_FLAGS)
|
|
||||||
CXXFLAGS = -std=c++1z -g -fPIC -O2 $(WARN_FLAGS)
|
|
||||||
|
|
||||||
CURL_FLAGS = -I$(PHONELIBS)/curl/include
|
|
||||||
CURL_LIBS = $(PHONELIBS)/curl/lib/libcurl.a \
|
|
||||||
$(PHONELIBS)/zlib/lib/libz.a
|
|
||||||
|
|
||||||
BORINGSSL_FLAGS = -I$(PHONELIBS)/boringssl/include
|
|
||||||
BORINGSSL_LIBS = $(PHONELIBS)/boringssl/lib/libssl_static.a \
|
|
||||||
$(PHONELIBS)/boringssl/lib/libcrypto_static.a \
|
|
||||||
|
|
||||||
NANOVG_FLAGS = -I$(PHONELIBS)/nanovg
|
|
||||||
|
|
||||||
JSON11_FLAGS = -I$(PHONELIBS)/json11
|
|
||||||
|
|
||||||
OPENGL_LIBS = -lGLESv3
|
|
||||||
|
|
||||||
FRAMEBUFFER_LIBS = -lutils -lgui -lEGL
|
|
||||||
|
|
||||||
.PHONY: all |
|
||||||
all: updater |
|
||||||
|
|
||||||
OBJS = opensans_regular.ttf.o \
|
|
||||||
opensans_semibold.ttf.o \
|
|
||||||
opensans_bold.ttf.o \
|
|
||||||
../../selfdrive/common/util.o \
|
|
||||||
../../selfdrive/common/touch.o \
|
|
||||||
../../selfdrive/common/framebuffer.o \
|
|
||||||
$(PHONELIBS)/json11/json11.o \
|
|
||||||
$(PHONELIBS)/nanovg/nanovg.o
|
|
||||||
|
|
||||||
DEPS := $(OBJS:.o=.d)
|
|
||||||
|
|
||||||
updater: updater.o $(OBJS) |
|
||||||
@echo "[ LINK ] $@"
|
|
||||||
$(CXX) $(CPPFLAGS) -fPIC -o 'updater' $^ \
|
|
||||||
$(FRAMEBUFFER_LIBS) \
|
|
||||||
$(CURL_LIBS) \
|
|
||||||
$(BORINGSSL_LIBS) \
|
|
||||||
-L/system/vendor/lib64 \
|
|
||||||
$(OPENGL_LIBS) \
|
|
||||||
-lcutils -lm -llog
|
|
||||||
strip updater
|
|
||||||
|
|
||||||
opensans_regular.ttf.o: ../../selfdrive/assets/fonts/opensans_regular.ttf |
|
||||||
@echo "[ bin2o ] $@"
|
|
||||||
cd '$(dir $<)' && ld -r -b binary '$(notdir $<)' -o '$(abspath $@)'
|
|
||||||
|
|
||||||
opensans_bold.ttf.o: ../../selfdrive/assets/fonts/opensans_bold.ttf |
|
||||||
@echo "[ bin2o ] $@"
|
|
||||||
cd '$(dir $<)' && ld -r -b binary '$(notdir $<)' -o '$(abspath $@)'
|
|
||||||
|
|
||||||
opensans_semibold.ttf.o: ../../selfdrive/assets/fonts/opensans_semibold.ttf |
|
||||||
@echo "[ bin2o ] $@"
|
|
||||||
cd '$(dir $<)' && ld -r -b binary '$(notdir $<)' -o '$(abspath $@)'
|
|
||||||
|
|
||||||
%.o: %.c |
|
||||||
mkdir -p $(@D)
|
|
||||||
@echo "[ CC ] $@"
|
|
||||||
$(CC) $(CPPFLAGS) $(CFLAGS) \
|
|
||||||
-I../.. \
|
|
||||||
-I$(PHONELIBS)/android_frameworks_native/include \
|
|
||||||
-I$(PHONELIBS)/android_system_core/include \
|
|
||||||
-I$(PHONELIBS)/android_hardware_libhardware/include \
|
|
||||||
$(NANOVG_FLAGS) \
|
|
||||||
-c -o '$@' '$<'
|
|
||||||
|
|
||||||
%.o: %.cc |
|
||||||
mkdir -p $(@D)
|
|
||||||
@echo "[ CXX ] $@"
|
|
||||||
$(CXX) $(CPPFLAGS) $(CXXFLAGS) \
|
|
||||||
-I../../selfdrive \
|
|
||||||
-I../../ \
|
|
||||||
-I$(PHONELIBS)/android_frameworks_native/include \
|
|
||||||
-I$(PHONELIBS)/android_system_core/include \
|
|
||||||
-I$(PHONELIBS)/android_hardware_libhardware/include \
|
|
||||||
$(NANOVG_FLAGS) \
|
|
||||||
$(JSON11_FLAGS) \
|
|
||||||
$(CURL_FLAGS) \
|
|
||||||
$(BORINGSSL_FLAGS) \
|
|
||||||
-c -o '$@' '$<'
|
|
||||||
|
|
||||||
|
|
||||||
.PHONY: clean |
|
||||||
clean: |
|
||||||
rm -f $(OBJS) $(DEPS)
|
|
||||||
|
|
||||||
-include $(DEPS) |
|
@ -1,82 +0,0 @@ |
|||||||
#!/usr/bin/env python3 |
|
||||||
import os |
|
||||||
import shutil |
|
||||||
import subprocess |
|
||||||
import tempfile |
|
||||||
import time |
|
||||||
import unittest |
|
||||||
|
|
||||||
from common.basedir import BASEDIR |
|
||||||
|
|
||||||
UPDATER_PATH = os.path.join(BASEDIR, "installer/updater") |
|
||||||
UPDATER = os.path.join(UPDATER_PATH, "updater") |
|
||||||
UPDATE_MANIFEST = os.path.join(UPDATER_PATH, "update.json") |
|
||||||
|
|
||||||
|
|
||||||
class TestUpdater(unittest.TestCase): |
|
||||||
|
|
||||||
@classmethod |
|
||||||
def setUpClass(cls): |
|
||||||
# test that the updater builds |
|
||||||
cls.assertTrue(f"cd {UPDATER_PATH} && make clean && make", "updater failed to build") |
|
||||||
|
|
||||||
# restore the checked-in version, since that's what actually runs on devices |
|
||||||
os.system(f"git reset --hard {UPDATER_PATH}") |
|
||||||
|
|
||||||
def setUp(self): |
|
||||||
self._clear_dir() |
|
||||||
|
|
||||||
def tearDown(self): |
|
||||||
self._clear_dir() |
|
||||||
|
|
||||||
def _clear_dir(self): |
|
||||||
if os.path.isdir("/data/neoupdate"): |
|
||||||
shutil.rmtree("/data/neoupdate") |
|
||||||
|
|
||||||
def _assert_ok(self, cmd, msg=None): |
|
||||||
self.assertTrue(os.system(cmd) == 0, msg) |
|
||||||
|
|
||||||
def _assert_fails(self, cmd): |
|
||||||
self.assertFalse(os.system(cmd) == 0) |
|
||||||
|
|
||||||
def test_background_download(self): |
|
||||||
self._assert_ok(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") |
|
||||||
|
|
||||||
def test_background_download_bad_manifest(self): |
|
||||||
# update with bad manifest should fail |
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".json") as f: |
|
||||||
f.write("{}") |
|
||||||
self._assert_fails(f"{UPDATER} bgcache 'file://{f.name}'") |
|
||||||
|
|
||||||
def test_cache_resume(self): |
|
||||||
self._assert_ok(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") |
|
||||||
# a full download takes >1m, but resuming from fully cached should only be a few seconds |
|
||||||
start_time = time.monotonic() |
|
||||||
self._assert_ok(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") |
|
||||||
self.assertLess(time.monotonic() - start_time, 10) |
|
||||||
|
|
||||||
# make sure we can recover from corrupt downloads |
|
||||||
def test_recover_from_corrupt(self): |
|
||||||
# download the whole update |
|
||||||
self._assert_ok(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") |
|
||||||
|
|
||||||
# write some random bytes |
|
||||||
for f in os.listdir("/data/neoupdate"): |
|
||||||
with open(os.path.join("/data/neoupdate", f), "ab") as f: |
|
||||||
f.write(b"\xab"*20) |
|
||||||
|
|
||||||
# this attempt should fail, then it unlinks |
|
||||||
self._assert_fails(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") |
|
||||||
|
|
||||||
# now it should pass |
|
||||||
self._assert_ok(f"{UPDATER} bgcache 'file://{UPDATE_MANIFEST}'") |
|
||||||
|
|
||||||
# simple test that the updater doesn't crash in UI mode |
|
||||||
def test_ui_init(self): |
|
||||||
with subprocess.Popen(UPDATER) as proc: |
|
||||||
time.sleep(5) |
|
||||||
self.assertTrue(proc.poll() is None) |
|
||||||
proc.terminate() |
|
||||||
|
|
||||||
if __name__ == "__main__": |
|
||||||
unittest.main() |
|
Binary file not shown.
@ -1,803 +0,0 @@ |
|||||||
#include <sys/stat.h> |
|
||||||
#include <sys/statvfs.h> |
|
||||||
#include <unistd.h> |
|
||||||
|
|
||||||
#include <cassert> |
|
||||||
#include <cstdio> |
|
||||||
#include <cstdlib> |
|
||||||
#include <cstring> |
|
||||||
#include <fstream> |
|
||||||
#include <iostream> |
|
||||||
#include <memory> |
|
||||||
#include <mutex> |
|
||||||
#include <sstream> |
|
||||||
#include <string> |
|
||||||
#include <thread> |
|
||||||
|
|
||||||
#include <curl/curl.h> |
|
||||||
#include <openssl/sha.h> |
|
||||||
#include <EGL/egl.h> |
|
||||||
#include <EGL/eglext.h> |
|
||||||
#include <GLES3/gl3.h> |
|
||||||
#include "nanovg.h" |
|
||||||
#define NANOVG_GLES3_IMPLEMENTATION |
|
||||||
#include "json11.hpp" |
|
||||||
#include "nanovg_gl.h" |
|
||||||
#include "nanovg_gl_utils.h" |
|
||||||
|
|
||||||
#include "selfdrive/common/framebuffer.h" |
|
||||||
#include "selfdrive/common/touch.h" |
|
||||||
#include "selfdrive/common/util.h" |
|
||||||
|
|
||||||
#define USER_AGENT "NEOSUpdater-0.2" |
|
||||||
|
|
||||||
#define MANIFEST_URL_NEOS_STAGING "https://github.com/commaai/eon-neos/raw/master/update.staging.json"
|
|
||||||
#define MANIFEST_URL_NEOS_LOCAL "http://192.168.5.1:8000/neosupdate/update.local.json"
|
|
||||||
#define MANIFEST_URL_NEOS "https://github.com/commaai/eon-neos/raw/master/update.json"
|
|
||||||
const char *manifest_url = MANIFEST_URL_NEOS; |
|
||||||
|
|
||||||
#define RECOVERY_DEV "/dev/block/bootdevice/by-name/recovery" |
|
||||||
#define RECOVERY_COMMAND "/cache/recovery/command" |
|
||||||
|
|
||||||
#define UPDATE_DIR "/data/neoupdate" |
|
||||||
|
|
||||||
extern const uint8_t bin_opensans_regular[] asm("_binary_opensans_regular_ttf_start"); |
|
||||||
extern const uint8_t bin_opensans_regular_end[] asm("_binary_opensans_regular_ttf_end"); |
|
||||||
extern const uint8_t bin_opensans_semibold[] asm("_binary_opensans_semibold_ttf_start"); |
|
||||||
extern const uint8_t bin_opensans_semibold_end[] asm("_binary_opensans_semibold_ttf_end"); |
|
||||||
extern const uint8_t bin_opensans_bold[] asm("_binary_opensans_bold_ttf_start"); |
|
||||||
extern const uint8_t bin_opensans_bold_end[] asm("_binary_opensans_bold_ttf_end"); |
|
||||||
|
|
||||||
namespace { |
|
||||||
|
|
||||||
std::string sha256_file(std::string fn, size_t limit=0) { |
|
||||||
SHA256_CTX ctx; |
|
||||||
SHA256_Init(&ctx); |
|
||||||
|
|
||||||
FILE *file = fopen(fn.c_str(), "rb"); |
|
||||||
if (!file) return ""; |
|
||||||
|
|
||||||
const size_t buf_size = 8192; |
|
||||||
std::unique_ptr<char[]> buffer( new char[ buf_size ] ); |
|
||||||
|
|
||||||
bool read_limit = (limit != 0); |
|
||||||
while (true) { |
|
||||||
size_t read_size = buf_size; |
|
||||||
if (read_limit) read_size = std::min(read_size, limit); |
|
||||||
size_t bytes_read = fread(buffer.get(), 1, read_size, file); |
|
||||||
if (!bytes_read) break; |
|
||||||
|
|
||||||
SHA256_Update(&ctx, buffer.get(), bytes_read); |
|
||||||
|
|
||||||
if (read_limit) { |
|
||||||
limit -= bytes_read; |
|
||||||
if (limit == 0) break; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
uint8_t hash[SHA256_DIGEST_LENGTH]; |
|
||||||
SHA256_Final(hash, &ctx); |
|
||||||
|
|
||||||
fclose(file); |
|
||||||
|
|
||||||
return util::tohex(hash, sizeof(hash)); |
|
||||||
} |
|
||||||
|
|
||||||
size_t download_string_write(void *ptr, size_t size, size_t nmeb, void *up) { |
|
||||||
size_t sz = size * nmeb; |
|
||||||
((std::string*)up)->append((char*)ptr, sz); |
|
||||||
return sz; |
|
||||||
} |
|
||||||
|
|
||||||
std::string download_string(CURL *curl, std::string url) { |
|
||||||
std::string os; |
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); |
|
||||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); |
|
||||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 0); |
|
||||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT); |
|
||||||
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1); |
|
||||||
curl_easy_setopt(curl, CURLOPT_RESUME_FROM, 0); |
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1); |
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, download_string_write); |
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &os); |
|
||||||
CURLcode res = curl_easy_perform(curl); |
|
||||||
if (res != CURLE_OK) { |
|
||||||
return ""; |
|
||||||
} |
|
||||||
|
|
||||||
return os; |
|
||||||
} |
|
||||||
|
|
||||||
size_t download_file_write(void *ptr, size_t size, size_t nmeb, void *up) { |
|
||||||
return fwrite(ptr, size, nmeb, (FILE*)up); |
|
||||||
} |
|
||||||
|
|
||||||
int battery_capacity() { |
|
||||||
std::string bat_cap_s = util::read_file("/sys/class/power_supply/battery/capacity"); |
|
||||||
return atoi(bat_cap_s.c_str()); |
|
||||||
} |
|
||||||
|
|
||||||
int battery_current() { |
|
||||||
std::string current_now_s = util::read_file("/sys/class/power_supply/battery/current_now"); |
|
||||||
return atoi(current_now_s.c_str()); |
|
||||||
} |
|
||||||
|
|
||||||
bool check_battery() { |
|
||||||
int bat_cap = battery_capacity(); |
|
||||||
int current_now = battery_current(); |
|
||||||
return bat_cap > 35 || (current_now < 0 && bat_cap > 10); |
|
||||||
} |
|
||||||
|
|
||||||
bool check_space() { |
|
||||||
struct statvfs stat; |
|
||||||
if (statvfs("/data/", &stat) != 0) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
size_t space = stat.f_bsize * stat.f_bavail; |
|
||||||
return space > 2000000000ULL; // 2GB
|
|
||||||
} |
|
||||||
|
|
||||||
static void start_settings_activity(const char* name) { |
|
||||||
char launch_cmd[1024]; |
|
||||||
snprintf(launch_cmd, sizeof(launch_cmd), |
|
||||||
"am start -W --ez :settings:show_fragment_as_subsetting true -n 'com.android.settings/.%s'", name); |
|
||||||
system(launch_cmd); |
|
||||||
} |
|
||||||
|
|
||||||
bool is_settings_active() { |
|
||||||
FILE *fp; |
|
||||||
char sys_output[4096]; |
|
||||||
|
|
||||||
fp = popen("/bin/dumpsys window windows", "r"); |
|
||||||
if (fp == NULL) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
bool active = false; |
|
||||||
while (fgets(sys_output, sizeof(sys_output), fp) != NULL) { |
|
||||||
if (strstr(sys_output, "mCurrentFocus=null") != NULL) { |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
if (strstr(sys_output, "mCurrentFocus=Window") != NULL) { |
|
||||||
active = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pclose(fp); |
|
||||||
|
|
||||||
return active; |
|
||||||
} |
|
||||||
|
|
||||||
struct Updater { |
|
||||||
bool do_exit = false; |
|
||||||
|
|
||||||
TouchState touch; |
|
||||||
|
|
||||||
int fb_w, fb_h; |
|
||||||
|
|
||||||
std::unique_ptr<FrameBuffer> fb; |
|
||||||
NVGcontext *vg = NULL; |
|
||||||
int font_regular; |
|
||||||
int font_semibold; |
|
||||||
int font_bold; |
|
||||||
|
|
||||||
std::thread update_thread_handle; |
|
||||||
|
|
||||||
std::mutex lock; |
|
||||||
|
|
||||||
enum UpdateState { |
|
||||||
CONFIRMATION, |
|
||||||
LOW_BATTERY, |
|
||||||
RUNNING, |
|
||||||
ERROR, |
|
||||||
}; |
|
||||||
UpdateState state; |
|
||||||
|
|
||||||
std::string progress_text; |
|
||||||
float progress_frac; |
|
||||||
|
|
||||||
std::string error_text; |
|
||||||
|
|
||||||
std::string low_battery_text; |
|
||||||
std::string low_battery_title; |
|
||||||
std::string low_battery_context; |
|
||||||
std::string battery_cap_text; |
|
||||||
int min_battery_cap = 35; |
|
||||||
|
|
||||||
// button
|
|
||||||
int b_x, b_w, b_y, b_h; |
|
||||||
int balt_x; |
|
||||||
|
|
||||||
// download stage writes these for the installation stage
|
|
||||||
int recovery_len; |
|
||||||
std::string recovery_hash; |
|
||||||
std::string recovery_fn; |
|
||||||
std::string ota_fn; |
|
||||||
|
|
||||||
CURL *curl = NULL; |
|
||||||
|
|
||||||
void ui_init() { |
|
||||||
touch_init(&touch); |
|
||||||
|
|
||||||
fb = std::make_unique<FrameBuffer>("updater", 0x00001000, false, &fb_w, &fb_h); |
|
||||||
|
|
||||||
fb->set_power(HWC_POWER_MODE_NORMAL); |
|
||||||
|
|
||||||
vg = nvgCreateGLES3(NVG_ANTIALIAS | NVG_STENCIL_STROKES | NVG_DEBUG); |
|
||||||
assert(vg); |
|
||||||
|
|
||||||
font_regular = nvgCreateFontMem(vg, "opensans_regular", (unsigned char*)bin_opensans_regular, (bin_opensans_regular_end - bin_opensans_regular), 0); |
|
||||||
assert(font_regular >= 0); |
|
||||||
|
|
||||||
font_semibold = nvgCreateFontMem(vg, "opensans_semibold", (unsigned char*)bin_opensans_semibold, (bin_opensans_semibold_end - bin_opensans_semibold), 0); |
|
||||||
assert(font_semibold >= 0); |
|
||||||
|
|
||||||
font_bold = nvgCreateFontMem(vg, "opensans_bold", (unsigned char*)bin_opensans_bold, (bin_opensans_bold_end - bin_opensans_bold), 0); |
|
||||||
assert(font_bold >= 0); |
|
||||||
|
|
||||||
b_w = 640; |
|
||||||
balt_x = 200; |
|
||||||
b_x = fb_w-b_w-200; |
|
||||||
b_y = 720; |
|
||||||
b_h = 220; |
|
||||||
|
|
||||||
if (download_stage(true)) { |
|
||||||
state = RUNNING; |
|
||||||
update_thread_handle = std::thread(&Updater::run_stages, this); |
|
||||||
} else { |
|
||||||
state = CONFIRMATION; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
int download_file_xferinfo(curl_off_t dltotal, curl_off_t dlno, |
|
||||||
curl_off_t ultotal, curl_off_t ulnow) { |
|
||||||
{ |
|
||||||
std::lock_guard<std::mutex> guard(lock); |
|
||||||
if (dltotal != 0) { |
|
||||||
progress_frac = (float) dlno / dltotal; |
|
||||||
} |
|
||||||
} |
|
||||||
// printf("info: %ld %ld %f\n", dltotal, dlno, progress_frac);
|
|
||||||
return 0; |
|
||||||
} |
|
||||||
|
|
||||||
bool download_file(std::string url, std::string out_fn) { |
|
||||||
FILE *of = fopen(out_fn.c_str(), "ab"); |
|
||||||
assert(of); |
|
||||||
|
|
||||||
CURLcode res; |
|
||||||
long last_resume_from = 0; |
|
||||||
|
|
||||||
fseek(of, 0, SEEK_END); |
|
||||||
|
|
||||||
int tries = 4; |
|
||||||
|
|
||||||
bool ret = false; |
|
||||||
|
|
||||||
while (true) { |
|
||||||
long resume_from = ftell(of); |
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); |
|
||||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); |
|
||||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 0); |
|
||||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT); |
|
||||||
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1); |
|
||||||
curl_easy_setopt(curl, CURLOPT_RESUME_FROM, resume_from); |
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, download_file_write); |
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, of); |
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); |
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this); |
|
||||||
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, &Updater::download_file_xferinfo); |
|
||||||
|
|
||||||
CURLcode res = curl_easy_perform(curl); |
|
||||||
|
|
||||||
long response_code = 0; |
|
||||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); |
|
||||||
|
|
||||||
// double content_length = 0.0;
|
|
||||||
// curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &content_length);
|
|
||||||
|
|
||||||
printf("download %s res %d, code %ld, resume from %ld\n", url.c_str(), res, response_code, resume_from); |
|
||||||
if (res == CURLE_OK) { |
|
||||||
ret = true; |
|
||||||
break; |
|
||||||
} else if (res == CURLE_HTTP_RETURNED_ERROR && response_code == 416) { |
|
||||||
// failed because the file is already complete?
|
|
||||||
ret = true; |
|
||||||
break; |
|
||||||
} else if (resume_from == last_resume_from) { |
|
||||||
// failed and dind't make make forward progress. only retry a couple times
|
|
||||||
tries--; |
|
||||||
if (tries <= 0) { |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
last_resume_from = resume_from; |
|
||||||
} |
|
||||||
// printf("res %d\n", res);
|
|
||||||
|
|
||||||
// printf("- %ld %f\n", response_code, content_length);
|
|
||||||
|
|
||||||
fclose(of); |
|
||||||
|
|
||||||
return ret; |
|
||||||
} |
|
||||||
|
|
||||||
void set_progress(std::string text) { |
|
||||||
std::lock_guard<std::mutex> guard(lock); |
|
||||||
progress_text = text; |
|
||||||
} |
|
||||||
|
|
||||||
void set_error(std::string text) { |
|
||||||
std::lock_guard<std::mutex> guard(lock); |
|
||||||
error_text = text; |
|
||||||
state = ERROR; |
|
||||||
} |
|
||||||
|
|
||||||
void set_battery_low() { |
|
||||||
std::lock_guard<std::mutex> guard(lock); |
|
||||||
state = LOW_BATTERY; |
|
||||||
} |
|
||||||
|
|
||||||
void set_running() { |
|
||||||
std::lock_guard<std::mutex> guard(lock); |
|
||||||
state = RUNNING; |
|
||||||
} |
|
||||||
|
|
||||||
std::string download(std::string url, std::string hash, std::string name, bool dry_run) { |
|
||||||
std::string out_fn = UPDATE_DIR "/" + util::base_name(url); |
|
||||||
|
|
||||||
std::string fn_hash = sha256_file(out_fn); |
|
||||||
if (dry_run) { |
|
||||||
return (hash.compare(fn_hash) != 0) ? "" : out_fn; |
|
||||||
} |
|
||||||
|
|
||||||
// start or resume downloading if hash doesn't match
|
|
||||||
if (hash.compare(fn_hash) != 0) { |
|
||||||
set_progress("Downloading " + name + "..."); |
|
||||||
bool r = download_file(url, out_fn); |
|
||||||
if (!r) { |
|
||||||
set_error("failed to download " + name); |
|
||||||
unlink(out_fn.c_str()); |
|
||||||
return ""; |
|
||||||
} |
|
||||||
fn_hash = sha256_file(out_fn); |
|
||||||
} |
|
||||||
|
|
||||||
set_progress("Verifying " + name + "..."); |
|
||||||
printf("got %s hash: %s\n", name.c_str(), hash.c_str()); |
|
||||||
if (fn_hash != hash) { |
|
||||||
set_error(name + " was corrupt"); |
|
||||||
unlink(out_fn.c_str()); |
|
||||||
return ""; |
|
||||||
} |
|
||||||
return out_fn; |
|
||||||
} |
|
||||||
|
|
||||||
bool download_stage(bool dry_run = false) { |
|
||||||
curl = curl_easy_init(); |
|
||||||
assert(curl); |
|
||||||
|
|
||||||
// ** quick checks before download **
|
|
||||||
|
|
||||||
if (!check_space()) { |
|
||||||
if (!dry_run) set_error("2GB of free space required to update"); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
mkdir(UPDATE_DIR, 0777); |
|
||||||
|
|
||||||
set_progress("Finding latest version..."); |
|
||||||
std::string manifest_s = download_string(curl, manifest_url); |
|
||||||
printf("manifest: %s\n", manifest_s.c_str()); |
|
||||||
|
|
||||||
std::string err; |
|
||||||
auto manifest = json11::Json::parse(manifest_s, err); |
|
||||||
if (manifest.is_null() || !err.empty()) { |
|
||||||
set_error("failed to load update manifest"); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
std::string ota_url = manifest["ota_url"].string_value(); |
|
||||||
std::string ota_hash = manifest["ota_hash"].string_value(); |
|
||||||
|
|
||||||
std::string recovery_url = manifest["recovery_url"].string_value(); |
|
||||||
recovery_hash = manifest["recovery_hash"].string_value(); |
|
||||||
recovery_len = manifest["recovery_len"].int_value(); |
|
||||||
|
|
||||||
// std::string installer_url = manifest["installer_url"].string_value();
|
|
||||||
// std::string installer_hash = manifest["installer_hash"].string_value();
|
|
||||||
|
|
||||||
if (ota_url.empty() || ota_hash.empty()) { |
|
||||||
set_error("invalid update manifest"); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
// std::string installer_fn = download(installer_url, installer_hash, "installer");
|
|
||||||
// if (installer_fn.empty()) {
|
|
||||||
// //error'd
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ** handle recovery download **
|
|
||||||
if (recovery_url.empty() || recovery_hash.empty() || recovery_len == 0) { |
|
||||||
set_progress("Skipping recovery flash..."); |
|
||||||
} else { |
|
||||||
// only download the recovery if it differs from what's flashed
|
|
||||||
set_progress("Checking recovery..."); |
|
||||||
std::string existing_recovery_hash = sha256_file(RECOVERY_DEV, recovery_len); |
|
||||||
printf("existing recovery hash: %s\n", existing_recovery_hash.c_str()); |
|
||||||
|
|
||||||
if (existing_recovery_hash != recovery_hash) { |
|
||||||
recovery_fn = download(recovery_url, recovery_hash, "recovery", dry_run); |
|
||||||
if (recovery_fn.empty()) { |
|
||||||
// error'd
|
|
||||||
return false; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// ** handle ota download **
|
|
||||||
ota_fn = download(ota_url, ota_hash, "update", dry_run); |
|
||||||
if (ota_fn.empty()) { |
|
||||||
//error'd
|
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
// download sucessful
|
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// thread that handles downloading and installing the update
|
|
||||||
void run_stages() { |
|
||||||
printf("run_stages start\n"); |
|
||||||
|
|
||||||
|
|
||||||
// ** download update **
|
|
||||||
|
|
||||||
if (!check_battery()) { |
|
||||||
set_battery_low(); |
|
||||||
int battery_cap = battery_capacity(); |
|
||||||
while(battery_cap < min_battery_cap) { |
|
||||||
battery_cap = battery_capacity(); |
|
||||||
battery_cap_text = std::to_string(battery_cap); |
|
||||||
util::sleep_for(1000); |
|
||||||
} |
|
||||||
set_running(); |
|
||||||
} |
|
||||||
|
|
||||||
bool sucess = download_stage(); |
|
||||||
if (!sucess) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// ** install update **
|
|
||||||
|
|
||||||
if (!check_battery()) { |
|
||||||
set_battery_low(); |
|
||||||
int battery_cap = battery_capacity(); |
|
||||||
while(battery_cap < min_battery_cap) { |
|
||||||
battery_cap = battery_capacity(); |
|
||||||
battery_cap_text = std::to_string(battery_cap); |
|
||||||
util::sleep_for(1000); |
|
||||||
} |
|
||||||
set_running(); |
|
||||||
} |
|
||||||
|
|
||||||
if (!recovery_fn.empty()) { |
|
||||||
// flash recovery
|
|
||||||
set_progress("Flashing recovery..."); |
|
||||||
|
|
||||||
FILE *flash_file = fopen(recovery_fn.c_str(), "rb"); |
|
||||||
if (!flash_file) { |
|
||||||
set_error("failed to flash recovery"); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
FILE *recovery_dev = fopen(RECOVERY_DEV, "w+b"); |
|
||||||
if (!recovery_dev) { |
|
||||||
fclose(flash_file); |
|
||||||
set_error("failed to flash recovery"); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const size_t buf_size = 4096; |
|
||||||
std::unique_ptr<char[]> buffer( new char[ buf_size ] ); |
|
||||||
|
|
||||||
while (true) { |
|
||||||
size_t bytes_read = fread(buffer.get(), 1, buf_size, flash_file); |
|
||||||
if (!bytes_read) break; |
|
||||||
|
|
||||||
size_t bytes_written = fwrite(buffer.get(), 1, bytes_read, recovery_dev); |
|
||||||
if (bytes_read != bytes_written) { |
|
||||||
fclose(recovery_dev); |
|
||||||
fclose(flash_file); |
|
||||||
set_error("failed to flash recovery: write failed"); |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fclose(recovery_dev); |
|
||||||
fclose(flash_file); |
|
||||||
|
|
||||||
set_progress("Verifying flash..."); |
|
||||||
std::string new_recovery_hash = sha256_file(RECOVERY_DEV, recovery_len); |
|
||||||
printf("new recovery hash: %s\n", new_recovery_hash.c_str()); |
|
||||||
|
|
||||||
if (new_recovery_hash != recovery_hash) { |
|
||||||
set_error("recovery flash corrupted"); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
// write arguments to recovery
|
|
||||||
FILE *cmd_file = fopen(RECOVERY_COMMAND, "wb"); |
|
||||||
if (!cmd_file) { |
|
||||||
set_error("failed to reboot into recovery"); |
|
||||||
return; |
|
||||||
} |
|
||||||
fprintf(cmd_file, "--update_package=%s\n", ota_fn.c_str()); |
|
||||||
fclose(cmd_file); |
|
||||||
|
|
||||||
set_progress("Rebooting"); |
|
||||||
|
|
||||||
// remove the continue.sh so we come back into the setup.
|
|
||||||
// maybe we should go directly into the installer, but what if we don't come back with internet? :/
|
|
||||||
//unlink("/data/data/com.termux/files/continue.sh");
|
|
||||||
|
|
||||||
// TODO: this should be generic between android versions
|
|
||||||
// IPowerManager.reboot(confirm=false, reason="recovery", wait=true)
|
|
||||||
system("service call power 16 i32 0 s16 recovery i32 1"); |
|
||||||
while (true) pause(); |
|
||||||
|
|
||||||
// execl("/system/bin/reboot", "recovery");
|
|
||||||
// set_error("failed to reboot into recovery");
|
|
||||||
} |
|
||||||
|
|
||||||
void draw_ack_screen(const char *title, const char *message, const char *button, const char *altbutton) { |
|
||||||
nvgFillColor(vg, nvgRGBA(255,255,255,255)); |
|
||||||
nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_BASELINE); |
|
||||||
|
|
||||||
nvgFontFace(vg, "opensans_bold"); |
|
||||||
nvgFontSize(vg, 120.0f); |
|
||||||
nvgTextBox(vg, 110, 220, fb_w-240, title, NULL); |
|
||||||
|
|
||||||
nvgFontFace(vg, "opensans_regular"); |
|
||||||
nvgFontSize(vg, 86.0f); |
|
||||||
nvgTextBox(vg, 130, 380, fb_w-260, message, NULL); |
|
||||||
|
|
||||||
// draw button
|
|
||||||
if (button) { |
|
||||||
nvgBeginPath(vg); |
|
||||||
nvgFillColor(vg, nvgRGBA(8, 8, 8, 255)); |
|
||||||
nvgRoundedRect(vg, b_x, b_y, b_w, b_h, 20); |
|
||||||
nvgFill(vg); |
|
||||||
|
|
||||||
nvgFillColor(vg, nvgRGBA(255, 255, 255, 255)); |
|
||||||
nvgFontFace(vg, "opensans_semibold"); |
|
||||||
nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); |
|
||||||
nvgText(vg, b_x+b_w/2, b_y+b_h/2, button, NULL); |
|
||||||
|
|
||||||
nvgBeginPath(vg); |
|
||||||
nvgStrokeColor(vg, nvgRGBA(255, 255, 255, 50)); |
|
||||||
nvgStrokeWidth(vg, 5); |
|
||||||
nvgRoundedRect(vg, b_x, b_y, b_w, b_h, 20); |
|
||||||
nvgStroke(vg); |
|
||||||
} |
|
||||||
|
|
||||||
// draw button
|
|
||||||
if (altbutton) { |
|
||||||
nvgBeginPath(vg); |
|
||||||
nvgFillColor(vg, nvgRGBA(8, 8, 8, 255)); |
|
||||||
nvgRoundedRect(vg, balt_x, b_y, b_w, b_h, 20); |
|
||||||
nvgFill(vg); |
|
||||||
|
|
||||||
nvgFillColor(vg, nvgRGBA(255, 255, 255, 255)); |
|
||||||
nvgFontFace(vg, "opensans_semibold"); |
|
||||||
nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); |
|
||||||
nvgText(vg, balt_x+b_w/2, b_y+b_h/2, altbutton, NULL); |
|
||||||
|
|
||||||
nvgBeginPath(vg); |
|
||||||
nvgStrokeColor(vg, nvgRGBA(255, 255, 255, 50)); |
|
||||||
nvgStrokeWidth(vg, 5); |
|
||||||
nvgRoundedRect(vg, balt_x, b_y, b_w, b_h, 20); |
|
||||||
nvgStroke(vg); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
void draw_battery_screen() { |
|
||||||
low_battery_title = "Low Battery"; |
|
||||||
low_battery_text = "Please connect EON to your charger. Update will continue once EON battery reaches 35%."; |
|
||||||
low_battery_context = "Current battery charge: " + battery_cap_text + "%"; |
|
||||||
|
|
||||||
nvgFillColor(vg, nvgRGBA(255,255,255,255)); |
|
||||||
nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_BASELINE); |
|
||||||
|
|
||||||
nvgFontFace(vg, "opensans_bold"); |
|
||||||
nvgFontSize(vg, 120.0f); |
|
||||||
nvgTextBox(vg, 110, 220, fb_w-240, low_battery_title.c_str(), NULL); |
|
||||||
|
|
||||||
nvgFontFace(vg, "opensans_regular"); |
|
||||||
nvgFontSize(vg, 86.0f); |
|
||||||
nvgTextBox(vg, 130, 380, fb_w-260, low_battery_text.c_str(), NULL); |
|
||||||
|
|
||||||
nvgFontFace(vg, "opensans_bold"); |
|
||||||
nvgFontSize(vg, 86.0f); |
|
||||||
nvgTextBox(vg, 130, 700, fb_w-260, low_battery_context.c_str(), NULL); |
|
||||||
} |
|
||||||
|
|
||||||
void draw_progress_screen() { |
|
||||||
// draw progress message
|
|
||||||
nvgFontSize(vg, 64.0f); |
|
||||||
nvgFillColor(vg, nvgRGBA(255,255,255,255)); |
|
||||||
nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_BASELINE); |
|
||||||
nvgFontFace(vg, "opensans_bold"); |
|
||||||
nvgFontSize(vg, 86.0f); |
|
||||||
nvgTextBox(vg, 0, 380, fb_w, progress_text.c_str(), NULL); |
|
||||||
|
|
||||||
// draw progress bar
|
|
||||||
{ |
|
||||||
int progress_width = 1000; |
|
||||||
int progress_x = fb_w/2-progress_width/2; |
|
||||||
int progress_y = 520; |
|
||||||
int progress_height = 50; |
|
||||||
|
|
||||||
int powerprompt_y = 312; |
|
||||||
nvgFontFace(vg, "opensans_regular"); |
|
||||||
nvgFontSize(vg, 64.0f); |
|
||||||
nvgText(vg, fb_w/2, 740, "Ensure your device remains connected to a power source.", NULL); |
|
||||||
|
|
||||||
NVGpaint paint = nvgBoxGradient( |
|
||||||
vg, progress_x + 1, progress_y + 1, |
|
||||||
progress_width - 2, progress_height, 3, 4, nvgRGB(27, 27, 27), nvgRGB(27, 27, 27)); |
|
||||||
nvgBeginPath(vg); |
|
||||||
nvgRoundedRect(vg, progress_x, progress_y, progress_width, progress_height, 12); |
|
||||||
nvgFillPaint(vg, paint); |
|
||||||
nvgFill(vg); |
|
||||||
|
|
||||||
float value = std::min(std::max(0.0f, progress_frac), 1.0f); |
|
||||||
int bar_pos = ((progress_width - 2) * value); |
|
||||||
|
|
||||||
paint = nvgBoxGradient( |
|
||||||
vg, progress_x, progress_y, |
|
||||||
bar_pos+1.5f, progress_height-1, 3, 4, |
|
||||||
nvgRGB(245, 245, 245), nvgRGB(105, 105, 105)); |
|
||||||
|
|
||||||
nvgBeginPath(vg); |
|
||||||
nvgRoundedRect( |
|
||||||
vg, progress_x+1, progress_y+1, |
|
||||||
bar_pos, progress_height-2, 12); |
|
||||||
nvgFillPaint(vg, paint); |
|
||||||
nvgFill(vg); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
void ui_draw() { |
|
||||||
std::lock_guard<std::mutex> guard(lock); |
|
||||||
|
|
||||||
nvgBeginFrame(vg, fb_w, fb_h, 1.0f); |
|
||||||
|
|
||||||
switch (state) { |
|
||||||
case CONFIRMATION: |
|
||||||
draw_ack_screen("An update to NEOS is required.", |
|
||||||
"Your device will now be reset and upgraded. You may want to connect to wifi as download is around 1 GB. Existing data on device should not be lost.", |
|
||||||
"Continue", |
|
||||||
"Connect to WiFi"); |
|
||||||
break; |
|
||||||
case LOW_BATTERY: |
|
||||||
draw_battery_screen(); |
|
||||||
break; |
|
||||||
case RUNNING: |
|
||||||
draw_progress_screen(); |
|
||||||
break; |
|
||||||
case ERROR: |
|
||||||
draw_ack_screen("There was an error", (error_text).c_str(), NULL, "Reboot"); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
nvgEndFrame(vg); |
|
||||||
} |
|
||||||
|
|
||||||
void ui_update() { |
|
||||||
std::lock_guard<std::mutex> guard(lock); |
|
||||||
|
|
||||||
if (state == ERROR || state == CONFIRMATION) { |
|
||||||
int touch_x = -1, touch_y = -1; |
|
||||||
int res = touch_poll(&touch, &touch_x, &touch_y, 0); |
|
||||||
if (res == 1 && !is_settings_active()) { |
|
||||||
if (touch_x >= b_x && touch_x < b_x+b_w && touch_y >= b_y && touch_y < b_y+b_h) { |
|
||||||
if (state == CONFIRMATION) { |
|
||||||
state = RUNNING; |
|
||||||
update_thread_handle = std::thread(&Updater::run_stages, this); |
|
||||||
} |
|
||||||
} |
|
||||||
if (touch_x >= balt_x && touch_x < balt_x+b_w && touch_y >= b_y && touch_y < b_y+b_h) { |
|
||||||
if (state == CONFIRMATION) { |
|
||||||
start_settings_activity("Settings$WifiSettingsActivity"); |
|
||||||
} else if (state == ERROR) { |
|
||||||
do_exit = 1; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
void go() { |
|
||||||
ui_init(); |
|
||||||
|
|
||||||
while (!do_exit) { |
|
||||||
ui_update(); |
|
||||||
|
|
||||||
glClearColor(0.08, 0.08, 0.08, 1.0); |
|
||||||
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); |
|
||||||
|
|
||||||
// background
|
|
||||||
nvgBeginPath(vg); |
|
||||||
NVGpaint bg = nvgLinearGradient(vg, fb_w, 0, fb_w, fb_h, |
|
||||||
nvgRGBA(0, 0, 0, 0), nvgRGBA(0, 0, 0, 255)); |
|
||||||
nvgFillPaint(vg, bg); |
|
||||||
nvgRect(vg, 0, 0, fb_w, fb_h); |
|
||||||
nvgFill(vg); |
|
||||||
|
|
||||||
glEnable(GL_BLEND); |
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); |
|
||||||
|
|
||||||
ui_draw(); |
|
||||||
|
|
||||||
glDisable(GL_BLEND); |
|
||||||
|
|
||||||
fb->swap(); |
|
||||||
|
|
||||||
assert(glGetError() == GL_NO_ERROR); |
|
||||||
|
|
||||||
// no simple way to do 30fps vsync with surfaceflinger...
|
|
||||||
util::sleep_for(30); |
|
||||||
} |
|
||||||
|
|
||||||
if (update_thread_handle.joinable()) { |
|
||||||
update_thread_handle.join(); |
|
||||||
} |
|
||||||
|
|
||||||
// reboot
|
|
||||||
system("service call power 16 i32 0 i32 0 i32 1"); |
|
||||||
} |
|
||||||
|
|
||||||
}; |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
int main(int argc, char *argv[]) { |
|
||||||
bool background_cache = false; |
|
||||||
if (argc > 1) { |
|
||||||
if (strcmp(argv[1], "local") == 0) { |
|
||||||
manifest_url = MANIFEST_URL_NEOS_LOCAL; |
|
||||||
} else if (strcmp(argv[1], "staging") == 0) { |
|
||||||
manifest_url = MANIFEST_URL_NEOS_STAGING; |
|
||||||
} else if (strcmp(argv[1], "bgcache") == 0) { |
|
||||||
manifest_url = argv[2]; |
|
||||||
background_cache = true; |
|
||||||
} else { |
|
||||||
manifest_url = argv[1]; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
printf("updating from %s\n", manifest_url); |
|
||||||
Updater updater; |
|
||||||
|
|
||||||
int err = 0; |
|
||||||
if (background_cache) { |
|
||||||
err = !updater.download_stage(); |
|
||||||
} else { |
|
||||||
updater.go(); |
|
||||||
} |
|
||||||
return err; |
|
||||||
} |
|
@ -1,146 +0,0 @@ |
|||||||
#include "selfdrive/common/framebuffer.h" |
|
||||||
|
|
||||||
#include <cstdio> |
|
||||||
#include <cassert> |
|
||||||
|
|
||||||
#include "selfdrive/common/util.h" |
|
||||||
|
|
||||||
#include <ui/DisplayInfo.h> |
|
||||||
|
|
||||||
#include <gui/ISurfaceComposer.h> |
|
||||||
#include <gui/Surface.h> |
|
||||||
#include <gui/SurfaceComposerClient.h> |
|
||||||
#include <GLES2/gl2.h> |
|
||||||
#include <EGL/eglext.h> |
|
||||||
|
|
||||||
#define BACKLIGHT_LEVEL 205 |
|
||||||
|
|
||||||
using namespace android; |
|
||||||
|
|
||||||
struct FramebufferState { |
|
||||||
sp<SurfaceComposerClient> session; |
|
||||||
sp<IBinder> dtoken; |
|
||||||
DisplayInfo dinfo; |
|
||||||
sp<SurfaceControl> control; |
|
||||||
|
|
||||||
sp<Surface> s; |
|
||||||
EGLDisplay display; |
|
||||||
|
|
||||||
EGLint egl_major, egl_minor; |
|
||||||
EGLConfig config; |
|
||||||
EGLSurface surface; |
|
||||||
EGLContext context; |
|
||||||
}; |
|
||||||
|
|
||||||
void FrameBuffer::swap() { |
|
||||||
eglSwapBuffers(s->display, s->surface); |
|
||||||
assert(glGetError() == GL_NO_ERROR); |
|
||||||
} |
|
||||||
|
|
||||||
bool set_brightness(int brightness) { |
|
||||||
char bright[64]; |
|
||||||
snprintf(bright, sizeof(bright), "%d", brightness); |
|
||||||
return 0 == util::write_file("/sys/class/leds/lcd-backlight/brightness", bright, strlen(bright)); |
|
||||||
} |
|
||||||
|
|
||||||
void FrameBuffer::set_power(int mode) { |
|
||||||
SurfaceComposerClient::setDisplayPowerMode(s->dtoken, mode); |
|
||||||
} |
|
||||||
|
|
||||||
FrameBuffer::FrameBuffer(const char *name, uint32_t layer, int alpha, int *out_w, int *out_h) { |
|
||||||
s = new FramebufferState; |
|
||||||
|
|
||||||
s->session = new SurfaceComposerClient(); |
|
||||||
assert(s->session != NULL); |
|
||||||
|
|
||||||
s->dtoken = SurfaceComposerClient::getBuiltInDisplay( |
|
||||||
ISurfaceComposer::eDisplayIdMain); |
|
||||||
assert(s->dtoken != NULL); |
|
||||||
|
|
||||||
status_t status = SurfaceComposerClient::getDisplayInfo(s->dtoken, &s->dinfo); |
|
||||||
assert(status == 0); |
|
||||||
|
|
||||||
//int orientation = 3; // rotate framebuffer 270 degrees
|
|
||||||
int orientation = 1; // rotate framebuffer 90 degrees
|
|
||||||
if(orientation == 1 || orientation == 3) { |
|
||||||
int temp = s->dinfo.h; |
|
||||||
s->dinfo.h = s->dinfo.w; |
|
||||||
s->dinfo.w = temp; |
|
||||||
} |
|
||||||
|
|
||||||
printf("dinfo %dx%d\n", s->dinfo.w, s->dinfo.h); |
|
||||||
|
|
||||||
Rect destRect(s->dinfo.w, s->dinfo.h); |
|
||||||
s->session->setDisplayProjection(s->dtoken, orientation, destRect, destRect); |
|
||||||
|
|
||||||
s->control = s->session->createSurface(String8(name), |
|
||||||
s->dinfo.w, s->dinfo.h, PIXEL_FORMAT_RGBX_8888); |
|
||||||
assert(s->control != NULL); |
|
||||||
|
|
||||||
SurfaceComposerClient::openGlobalTransaction(); |
|
||||||
status = s->control->setLayer(layer); |
|
||||||
SurfaceComposerClient::closeGlobalTransaction(); |
|
||||||
assert(status == 0); |
|
||||||
|
|
||||||
s->s = s->control->getSurface(); |
|
||||||
assert(s->s != NULL); |
|
||||||
|
|
||||||
// init opengl and egl
|
|
||||||
const EGLint attribs[] = { |
|
||||||
EGL_RED_SIZE, 8, |
|
||||||
EGL_GREEN_SIZE, 8, |
|
||||||
EGL_BLUE_SIZE, 8, |
|
||||||
EGL_ALPHA_SIZE, alpha ? 8 : 0, |
|
||||||
EGL_DEPTH_SIZE, 0, |
|
||||||
EGL_STENCIL_SIZE, 8, |
|
||||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR, |
|
||||||
// enable MSAA
|
|
||||||
EGL_SAMPLE_BUFFERS, 1, |
|
||||||
EGL_SAMPLES, 4, |
|
||||||
EGL_NONE, |
|
||||||
}; |
|
||||||
|
|
||||||
s->display = eglGetDisplay(EGL_DEFAULT_DISPLAY); |
|
||||||
assert(s->display != EGL_NO_DISPLAY); |
|
||||||
|
|
||||||
int success = eglInitialize(s->display, &s->egl_major, &s->egl_minor); |
|
||||||
assert(success); |
|
||||||
|
|
||||||
printf("egl version %d.%d\n", s->egl_major, s->egl_minor); |
|
||||||
|
|
||||||
EGLint num_configs; |
|
||||||
success = eglChooseConfig(s->display, attribs, &s->config, 1, &num_configs); |
|
||||||
assert(success); |
|
||||||
|
|
||||||
s->surface = eglCreateWindowSurface(s->display, s->config, s->s.get(), NULL); |
|
||||||
assert(s->surface != EGL_NO_SURFACE); |
|
||||||
|
|
||||||
const EGLint context_attribs[] = { |
|
||||||
EGL_CONTEXT_CLIENT_VERSION, 3, |
|
||||||
EGL_NONE, |
|
||||||
}; |
|
||||||
s->context = eglCreateContext(s->display, s->config, NULL, context_attribs); |
|
||||||
assert(s->context != EGL_NO_CONTEXT); |
|
||||||
|
|
||||||
EGLint w, h; |
|
||||||
eglQuerySurface(s->display, s->surface, EGL_WIDTH, &w); |
|
||||||
eglQuerySurface(s->display, s->surface, EGL_HEIGHT, &h); |
|
||||||
printf("egl w %d h %d\n", w, h); |
|
||||||
|
|
||||||
success = eglMakeCurrent(s->display, s->surface, s->surface, s->context); |
|
||||||
assert(success); |
|
||||||
|
|
||||||
printf("gl version %s\n", glGetString(GL_VERSION)); |
|
||||||
|
|
||||||
set_brightness(BACKLIGHT_LEVEL); |
|
||||||
|
|
||||||
if (out_w) *out_w = w; |
|
||||||
if (out_h) *out_h = h; |
|
||||||
} |
|
||||||
|
|
||||||
FrameBuffer::~FrameBuffer() { |
|
||||||
eglDestroyContext(s->display, s->context); |
|
||||||
eglDestroySurface(s->display, s->surface); |
|
||||||
eglTerminate(s->display); |
|
||||||
delete s; |
|
||||||
} |
|
@ -1,18 +0,0 @@ |
|||||||
#pragma once |
|
||||||
|
|
||||||
#include <cstdlib> |
|
||||||
|
|
||||||
#include "hardware/hwcomposer_defs.h" |
|
||||||
|
|
||||||
bool set_brightness(int brightness); |
|
||||||
|
|
||||||
struct FramebufferState; |
|
||||||
class FrameBuffer { |
|
||||||
public: |
|
||||||
FrameBuffer(const char *name, uint32_t layer, int alpha, int *out_w, int *out_h); |
|
||||||
~FrameBuffer(); |
|
||||||
void set_power(int mode); |
|
||||||
void swap(); |
|
||||||
private: |
|
||||||
FramebufferState *s; |
|
||||||
}; |
|
@ -1,96 +0,0 @@ |
|||||||
#include "selfdrive/common/touch.h" |
|
||||||
|
|
||||||
#include <assert.h> |
|
||||||
#include <dirent.h> |
|
||||||
#include <fcntl.h> |
|
||||||
#include <linux/input.h> |
|
||||||
#include <stdbool.h> |
|
||||||
#include <stdio.h> |
|
||||||
#include <stdlib.h> |
|
||||||
#include <sys/poll.h> |
|
||||||
#include <unistd.h> |
|
||||||
|
|
||||||
/* this macro is used to tell if "bit" is set in "array"
|
|
||||||
* it selects a byte from the array, and does a boolean AND |
|
||||||
* operation with a byte that only has the relevant bit set. |
|
||||||
* eg. to check for the 12th bit, we do (array[1] & 1<<4) |
|
||||||
*/ |
|
||||||
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8))) |
|
||||||
|
|
||||||
static int find_dev() { |
|
||||||
int err; |
|
||||||
|
|
||||||
int ret = -1; |
|
||||||
|
|
||||||
DIR *dir = opendir("/dev/input"); |
|
||||||
assert(dir); |
|
||||||
struct dirent* de = NULL; |
|
||||||
while ((de = readdir(dir))) { |
|
||||||
if (strncmp(de->d_name, "event", 5)) continue; |
|
||||||
|
|
||||||
int fd = openat(dirfd(dir), de->d_name, O_RDONLY); |
|
||||||
assert(fd >= 0); |
|
||||||
|
|
||||||
unsigned char ev_bits[KEY_MAX / 8 + 1]; |
|
||||||
err = ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(ev_bits)), ev_bits); |
|
||||||
assert(err >= 0); |
|
||||||
|
|
||||||
if (test_bit(ABS_MT_POSITION_X, ev_bits) && test_bit(ABS_MT_POSITION_Y, ev_bits)) { |
|
||||||
ret = fd; |
|
||||||
break; |
|
||||||
} |
|
||||||
close(fd); |
|
||||||
} |
|
||||||
closedir(dir); |
|
||||||
|
|
||||||
return ret; |
|
||||||
} |
|
||||||
|
|
||||||
void touch_init(TouchState *s) { |
|
||||||
s->fd = find_dev(); |
|
||||||
assert(s->fd >= 0); |
|
||||||
} |
|
||||||
|
|
||||||
int touch_poll(TouchState *s, int* out_x, int* out_y, int timeout) { |
|
||||||
assert(out_x && out_y); |
|
||||||
bool up = false; |
|
||||||
while (true) { |
|
||||||
struct pollfd polls[] = {{ |
|
||||||
.fd = s->fd, |
|
||||||
.events = POLLIN, |
|
||||||
}}; |
|
||||||
int err = poll(polls, 1, timeout); |
|
||||||
if (err < 0) { |
|
||||||
return -1; |
|
||||||
} |
|
||||||
if (!(polls[0].revents & POLLIN)) { |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
struct input_event event; |
|
||||||
err = read(polls[0].fd, &event, sizeof(event)); |
|
||||||
if (err < sizeof(event)) { |
|
||||||
return -1; |
|
||||||
} |
|
||||||
|
|
||||||
switch (event.type) { |
|
||||||
case EV_ABS: |
|
||||||
if (event.code == ABS_MT_POSITION_X) { |
|
||||||
s->last_x = event.value; |
|
||||||
} else if (event.code == ABS_MT_POSITION_Y) { |
|
||||||
s->last_y = event.value; |
|
||||||
} else if (event.code == ABS_MT_TRACKING_ID && event.value != -1) { |
|
||||||
up = true; |
|
||||||
} |
|
||||||
break; |
|
||||||
default: |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
if (up) { |
|
||||||
// adjust for flippening
|
|
||||||
*out_x = s->last_y; |
|
||||||
*out_y = 1080 - s->last_x; |
|
||||||
} |
|
||||||
return up; |
|
||||||
} |
|
@ -1,17 +0,0 @@ |
|||||||
#pragma once |
|
||||||
|
|
||||||
#ifdef __cplusplus |
|
||||||
extern "C" { |
|
||||||
#endif |
|
||||||
|
|
||||||
typedef struct TouchState { |
|
||||||
int fd; |
|
||||||
int last_x, last_y; |
|
||||||
} TouchState; |
|
||||||
|
|
||||||
void touch_init(TouchState *s); |
|
||||||
int touch_poll(TouchState *s, int *out_x, int *out_y, int timeout); |
|
||||||
|
|
||||||
#ifdef __cplusplus |
|
||||||
} |
|
||||||
#endif |
|
@ -0,0 +1,130 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
import argparse |
||||||
|
import hashlib |
||||||
|
import json |
||||||
|
import logging |
||||||
|
import os |
||||||
|
import requests |
||||||
|
|
||||||
|
NEOSUPDATE_DIR = "/data/neoupdate" |
||||||
|
|
||||||
|
RECOVERY_DEV = "/dev/block/bootdevice/by-name/recovery" |
||||||
|
RECOVERY_COMMAND = "/cache/recovery/command" |
||||||
|
|
||||||
|
|
||||||
|
def get_fn(url: str): |
||||||
|
return os.path.join(NEOSUPDATE_DIR, os.path.basename(url)) |
||||||
|
|
||||||
|
|
||||||
|
def download_file(url: str, fn: str, sha256: str, display_name: str, cloudlog=logging) -> None: |
||||||
|
# check if already downloaded |
||||||
|
if check_hash(fn, sha256): |
||||||
|
cloudlog.info(f"{display_name} already cached") |
||||||
|
return |
||||||
|
|
||||||
|
try: |
||||||
|
with open(fn, "ab+") as f: |
||||||
|
headers = {"Range": f"bytes={f.tell()}-"} |
||||||
|
r = requests.get(url, stream=True, allow_redirects=True, headers=headers) |
||||||
|
r.raise_for_status() |
||||||
|
|
||||||
|
total = int(r.headers['Content-Length']) |
||||||
|
if 'Content-Range' in r.headers: |
||||||
|
total = int(r.headers['Content-Range'].split('/')[-1]) |
||||||
|
|
||||||
|
for chunk in r.iter_content(chunk_size=1024 * 1024): |
||||||
|
f.write(chunk) |
||||||
|
print(f"Downloading {display_name}: {f.tell() / total * 100}") |
||||||
|
except Exception: |
||||||
|
cloudlog.error("download error") |
||||||
|
if os.path.isfile(fn): |
||||||
|
os.unlink(fn) |
||||||
|
raise |
||||||
|
|
||||||
|
if not check_hash(fn, sha256): |
||||||
|
if os.path.isfile(fn): |
||||||
|
os.unlink(fn) |
||||||
|
raise Exception("downloaded update failed hash check") |
||||||
|
|
||||||
|
|
||||||
|
def check_hash(fn: str, sha256: str, length: int = -1) -> bool: |
||||||
|
if not os.path.exists(fn): |
||||||
|
return False |
||||||
|
|
||||||
|
h = hashlib.sha256() |
||||||
|
with open(fn, "rb") as f: |
||||||
|
while f.tell() != length: |
||||||
|
r = min(max(0, length - f.tell()), 1024 * 1024) if length > 0 else 1024 * 1024 |
||||||
|
dat = f.read(r) |
||||||
|
if not dat: |
||||||
|
break |
||||||
|
h.update(dat) |
||||||
|
return h.hexdigest().lower() == sha256.lower() |
||||||
|
|
||||||
|
|
||||||
|
def flash_update(update_fn: str, out_path: str) -> None: |
||||||
|
with open(update_fn, "rb") as update, open(out_path, "w+b") as out: |
||||||
|
while True: |
||||||
|
dat = update.read(8192) |
||||||
|
if len(dat) == 0: |
||||||
|
break |
||||||
|
out.write(dat) |
||||||
|
|
||||||
|
|
||||||
|
def download_neos_update(manifest_path: str, cloudlog=logging) -> None: |
||||||
|
with open(manifest_path) as f: |
||||||
|
m = json.load(f) |
||||||
|
|
||||||
|
os.makedirs(NEOSUPDATE_DIR, exist_ok=True) |
||||||
|
|
||||||
|
# handle recovery updates |
||||||
|
if not check_hash(RECOVERY_DEV, m['recovery_hash'], m['recovery_len']): |
||||||
|
cloudlog.info("recovery needs update") |
||||||
|
recovery_fn = os.path.join(NEOSUPDATE_DIR, os.path.basename(m['recovery_url'])) |
||||||
|
download_file(m['recovery_url'], recovery_fn, m['recovery_hash'], "recovery", cloudlog) |
||||||
|
|
||||||
|
flash_update(recovery_fn, RECOVERY_DEV) |
||||||
|
assert check_hash(RECOVERY_DEV, m['recovery_hash'], m['recovery_len']), "recovery flash corrupted" |
||||||
|
cloudlog.info("recovery successfully flashed") |
||||||
|
|
||||||
|
# download OTA update |
||||||
|
download_file(m['ota_url'], get_fn(m['ota_url']), m['ota_hash'], "system", cloudlog) |
||||||
|
|
||||||
|
|
||||||
|
def verify_update_ready(manifest_path: str) -> bool: |
||||||
|
with open(manifest_path) as f: |
||||||
|
m = json.load(f) |
||||||
|
|
||||||
|
ota_downloaded = check_hash(get_fn(m['ota_url']), m['ota_hash']) |
||||||
|
recovery_flashed = check_hash(RECOVERY_DEV, m['recovery_hash'], m['recovery_len']) |
||||||
|
return ota_downloaded and recovery_flashed |
||||||
|
|
||||||
|
|
||||||
|
def perform_ota_update(manifest_path: str) -> None: |
||||||
|
with open(manifest_path) as f: |
||||||
|
m = json.load(f) |
||||||
|
|
||||||
|
# reboot into recovery |
||||||
|
ota_fn = get_fn(m['ota_url']) |
||||||
|
with open(RECOVERY_COMMAND, "w") as rf: |
||||||
|
rf.write(f"--update_package={ota_fn}\n") |
||||||
|
os.system("service call power 16 i32 0 s16 recovery i32 1") |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
parser = argparse.ArgumentParser(description="NEOS update utility", |
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
||||||
|
parser.add_argument("--swap", action="store_true", help="Peform update after downloading") |
||||||
|
parser.add_argument("--swap-if-ready", action="store_true", help="Perform update if already downloaded") |
||||||
|
parser.add_argument("manifest", help="Manifest json") |
||||||
|
args = parser.parse_args() |
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO) |
||||||
|
|
||||||
|
if args.swap_if_ready: |
||||||
|
if verify_update_ready(args.manifest): |
||||||
|
perform_ota_update(args.manifest) |
||||||
|
else: |
||||||
|
download_neos_update(args.manifest, logging) |
||||||
|
if args.swap: |
||||||
|
perform_ota_update(args.manifest) |
@ -0,0 +1,145 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
import hashlib |
||||||
|
import http.server |
||||||
|
import json |
||||||
|
import os |
||||||
|
import unittest |
||||||
|
import random |
||||||
|
import requests |
||||||
|
import shutil |
||||||
|
import socketserver |
||||||
|
import tempfile |
||||||
|
import multiprocessing |
||||||
|
from pathlib import Path |
||||||
|
|
||||||
|
from selfdrive.hardware.eon.neos import RECOVERY_DEV, NEOSUPDATE_DIR, get_fn, download_file, \ |
||||||
|
verify_update_ready, download_neos_update |
||||||
|
|
||||||
|
EON_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__))) |
||||||
|
MANIFEST = os.path.join(EON_DIR, "neos.json") |
||||||
|
|
||||||
|
PORT = 8000 |
||||||
|
|
||||||
|
def server_thread(port): |
||||||
|
socketserver.TCPServer.allow_reuse_address = True |
||||||
|
httpd = socketserver.TCPServer(("", port), http.server.SimpleHTTPRequestHandler) |
||||||
|
httpd.serve_forever() |
||||||
|
|
||||||
|
|
||||||
|
class TestNeosUpdater(unittest.TestCase): |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def setUpClass(cls): |
||||||
|
# generate a fake manifest |
||||||
|
cls.manifest = {} |
||||||
|
for i in ('ota', 'recovery'): |
||||||
|
with tempfile.NamedTemporaryFile(delete=False, dir=os.getcwd()) as f: |
||||||
|
dat = os.urandom(random.randint(1000, 100000)) |
||||||
|
f.write(dat) |
||||||
|
cls.manifest[f"{i}_url"] = f"http://localhost:{PORT}/" + os.path.relpath(f.name) |
||||||
|
cls.manifest[F"{i}_hash"] = hashlib.sha256(dat).hexdigest() |
||||||
|
if i == "recovery": |
||||||
|
cls.manifest["recovery_len"] = len(dat) |
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, mode='w') as f: |
||||||
|
f.write(json.dumps(cls.manifest)) |
||||||
|
cls.fake_manifest = f.name |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def tearDownClass(cls): |
||||||
|
os.unlink(cls.fake_manifest) |
||||||
|
os.unlink(os.path.basename(cls.manifest['ota_url'])) |
||||||
|
os.unlink(os.path.basename(cls.manifest['recovery_url'])) |
||||||
|
|
||||||
|
def setUp(self): |
||||||
|
# server for update files |
||||||
|
self.server = multiprocessing.Process(target=server_thread, args=(PORT, )) |
||||||
|
self.server.start() |
||||||
|
|
||||||
|
# clean up |
||||||
|
if os.path.exists(NEOSUPDATE_DIR): |
||||||
|
shutil.rmtree(NEOSUPDATE_DIR) |
||||||
|
|
||||||
|
def tearDown(self): |
||||||
|
self.server.kill() |
||||||
|
self.server.join(1) |
||||||
|
|
||||||
|
def _corrupt_recovery(self): |
||||||
|
with open(RECOVERY_DEV, "wb") as f: |
||||||
|
f.write(b'\x00'*100) |
||||||
|
|
||||||
|
def test_manifest(self): |
||||||
|
with open(MANIFEST) as f: |
||||||
|
m = json.load(f) |
||||||
|
|
||||||
|
assert m['ota_hash'] in m['ota_url'] |
||||||
|
assert m['recovery_hash'] in m['recovery_url'] |
||||||
|
assert m['recovery_len'] > 0 |
||||||
|
|
||||||
|
for url in (m['ota_url'], m['recovery_url']): |
||||||
|
r = requests.head(m['recovery_url']) |
||||||
|
r.raise_for_status() |
||||||
|
self.assertEqual(r.headers['Content-Type'], "application/octet-stream") |
||||||
|
if url == m['recovery_url']: |
||||||
|
self.assertEqual(int(r.headers['Content-Length']), m['recovery_len']) |
||||||
|
|
||||||
|
def test_download_hash_check(self): |
||||||
|
os.makedirs(NEOSUPDATE_DIR, exist_ok=True) |
||||||
|
Path(get_fn(self.manifest['ota_url'])).touch() |
||||||
|
with self.assertRaisesRegex(Exception, "failed hash check"): |
||||||
|
download_file(self.manifest['ota_url'], get_fn(self.manifest['ota_url']), |
||||||
|
self.manifest['ota_hash']+'a', "system") |
||||||
|
|
||||||
|
# should've unlinked after the failed hash check, should succeed now |
||||||
|
download_file(self.manifest['ota_url'], get_fn(self.manifest['ota_url']), |
||||||
|
self.manifest['ota_hash'], "system") |
||||||
|
|
||||||
|
# TODO: needs an http server that supports Content-Range |
||||||
|
#def test_download_resume(self): |
||||||
|
# os.makedirs(NEOSUPDATE_DIR, exist_ok=True) |
||||||
|
# with open(os.path.basename(self.manifest['ota_url']), "rb") as src, \ |
||||||
|
# open(get_fn(self.manifest['ota_url']), "wb") as dest: |
||||||
|
# l = dest.write(src.read(8192)) |
||||||
|
# assert l == 8192 |
||||||
|
# download_file(self.manifest['ota_url'], get_fn(self.manifest['ota_url']), |
||||||
|
# self.manifest['ota_hash'], "system") |
||||||
|
|
||||||
|
def test_download_no_internet(self): |
||||||
|
self.server.kill() |
||||||
|
os.makedirs(NEOSUPDATE_DIR, exist_ok=True) |
||||||
|
# fail, no internet |
||||||
|
with self.assertRaises(requests.exceptions.ConnectionError): |
||||||
|
download_file(self.manifest['ota_url'], get_fn(self.manifest['ota_url']), |
||||||
|
self.manifest['ota_hash'], "system") |
||||||
|
|
||||||
|
# already cached, ensure we don't hit the server |
||||||
|
shutil.copyfile(os.path.basename(self.manifest['ota_url']), get_fn(self.manifest['ota_url'])) |
||||||
|
download_file(self.manifest['ota_url'], get_fn(self.manifest['ota_url']), |
||||||
|
self.manifest['ota_hash'], "system") |
||||||
|
|
||||||
|
|
||||||
|
def test_download_update(self): |
||||||
|
download_neos_update(self.fake_manifest) |
||||||
|
self.assertTrue(verify_update_ready(self.fake_manifest)) |
||||||
|
|
||||||
|
def test_verify_update(self): |
||||||
|
# good state |
||||||
|
download_neos_update(self.fake_manifest) |
||||||
|
self.assertTrue(verify_update_ready(self.fake_manifest)) |
||||||
|
|
||||||
|
# corrupt recovery |
||||||
|
self._corrupt_recovery() |
||||||
|
self.assertFalse(verify_update_ready(self.fake_manifest)) |
||||||
|
|
||||||
|
# back to good state |
||||||
|
download_neos_update(self.fake_manifest) |
||||||
|
self.assertTrue(verify_update_ready(self.fake_manifest)) |
||||||
|
|
||||||
|
# corrupt ota |
||||||
|
self._corrupt_recovery() |
||||||
|
with open(os.path.join(NEOSUPDATE_DIR, os.path.basename(self.manifest['ota_url'])), "ab") as f: |
||||||
|
f.write(b'\x00') |
||||||
|
self.assertFalse(verify_update_ready(self.fake_manifest)) |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
unittest.main() |
Binary file not shown.
Loading…
Reference in new issue