From b720ec98740399cebe1901ee1ea369d8986c5520 Mon Sep 17 00:00:00 2001 From: Zhiquan Yeo Date: Thu, 10 Dec 2020 07:44:48 -0500 Subject: [PATCH] Add support for offline updates of Romi webservice (#169) --- deps/tools/configServer/Makefile | 6 +- deps/tools/configServer/src/RomiStatus.cpp | 14 ++--- .../configServer/src/WebSocketHandlers.cpp | 62 +++++++++++++++++++ .../configServer/src/resources/frcvision.js | 62 ++++++++++++++++++- .../configServer/src/resources/index.html | 25 ++++++++ 5 files changed, 157 insertions(+), 12 deletions(-) diff --git a/deps/tools/configServer/Makefile b/deps/tools/configServer/Makefile index 54d2d61d0a..aa0ab2015f 100644 --- a/deps/tools/configServer/Makefile +++ b/deps/tools/configServer/Makefile @@ -1,7 +1,7 @@ IMG_VERSION?=$(shell git describe) DEPS_CFLAGS?=$(shell pkg-config --cflags cscore wpiutil) -std=c++17 DEPS_LIBS?=$(shell pkg-config --libs --static cscore wpiutil) -EXEC_HOME?=/home/${FIRST_USER_NAME} +EXEC_HOME?=/home/pi FRC_JSON?=/boot/frc.json DHCPCD_CONF?=/boot/dhcpcd.conf WPA_SUPPLICANT_CONF?=/etc/wpa_supplicant/wpa_supplicant.conf @@ -9,6 +9,8 @@ DNSMASQ_CONF?=/etc/dnsmasq.d/wpilib.conf HOSTAPD_CONF?=/etc/hostapd/hostapd.conf APP_UID?=1000 APP_GID?=1000 +ROMI_JSON?=/boot/romi.json +NODE_HOME?=/home/pi/.nvm/versions/node/v14.15.0/bin .PHONY: all clean install .SUFFIXES: @@ -56,6 +58,8 @@ configServer: ${OBJS} '-DHOSTAPD_CONF="${HOSTAPD_CONF}"' \ '-DAPP_UID=${APP_UID}' \ '-DAPP_GID=${APP_GID}' \ + '-DROMI_JSON="${ROMI_JSON}"' \ + '-DNODE_HOME="${NODE_HOME}"' \ $< %.html.cpp: %.html diff --git a/deps/tools/configServer/src/RomiStatus.cpp b/deps/tools/configServer/src/RomiStatus.cpp index 766b7630f0..dcc41221c8 100644 --- a/deps/tools/configServer/src/RomiStatus.cpp +++ b/deps/tools/configServer/src/RomiStatus.cpp @@ -33,12 +33,6 @@ namespace uv = wpi::uv; #define SERVICE "/service/wpilibws-romi" -#ifdef __RASPBIAN__ -static const char* configFile = "/boot/romi.json"; -#else -static const char* configFile = "romi.json"; -#endif - std::shared_ptr RomiStatus::GetInstance() { static auto status = std::make_shared(private_init{}); return status; @@ -243,11 +237,11 @@ void RomiStatus::UpdateIoConfig(std::function onFail) { wpi::json RomiStatus::GetIoConfigJson(std::function onFail) { // Read config file std::error_code ec; - wpi::raw_fd_istream is(configFile, ec); + wpi::raw_fd_istream is(ROMI_JSON, ec); if (ec) { onFail("Could not read romi config file"); - wpi::errs() << "could not read " << configFile << "\n"; + wpi::errs() << "could not read " << ROMI_JSON << "\n"; wpi::json(); } @@ -256,7 +250,7 @@ wpi::json RomiStatus::GetIoConfigJson(std::function onFail j = wpi::json::parse(is); } catch(const wpi::json::parse_error& e) { onFail("Parse error in config file"); - wpi::errs() << "Parse error in " << configFile << ": byte " + wpi::errs() << "Parse error in " << ROMI_JSON << ": byte " << e.byte <<": " << e.what() << "\n"; return wpi::json(); } @@ -275,7 +269,7 @@ void RomiStatus::SaveConfig(const wpi::json& data, std::function(); + d->upload.Open(EXEC_HOME "/romiUploadXXXXXX", false, statusFunc); + } + else if (subType == "ServiceFinishUpload") { + auto d = ws.GetData(); + + if (fchown(d->upload.GetFD(), APP_UID, APP_GID) == -1) { + wpi::errs() << "could not change file ownership: " + << std::strerror(errno) << "\n"; + } + d->upload.Close(); + + std::string filename; + try { + filename = j.at("fileName").get(); + } catch (const wpi::json::exception& e) { + wpi::errs() << "could not read fileName value: " << e.what() << "\n"; + unlink(d->upload.GetFilename()); + return; + } + + wpi::SmallString<64> pathname; + pathname = EXEC_HOME; + pathname += '/'; + pathname += filename; + if (unlink(pathname.c_str()) == -1) { + wpi::errs() << "could not remove file: " << std::strerror(errno) + << "\n"; + } + + // Rename temporary file to new file + if (rename(d->upload.GetFilename(), pathname.c_str()) == -1) { + wpi::errs() << "could not rename: " << std::strerror(errno) << "\n"; + } + + auto installSuccess = [sf = statusFunc](wpi::WebSocket& s) { + auto d = s.GetData(); + unlink(d->upload.GetFilename()); + SendWsText(s, {{"type", "romiServiceUploadComplete"}, {"success", true}}); + RomiStatus::GetInstance()->Up(sf); + }; + + auto installFailure = [sf = statusFunc](wpi::WebSocket& s) { + auto d = s.GetData(); + unlink(d->upload.GetFilename()); + wpi::errs() << "could not install service\n"; + SendWsText(s, {{"type", "romiServiceUploadComplete"}}); + RomiStatus::GetInstance()->Up(sf); + }; + + RomiStatus::GetInstance()->Down(statusFunc); + RunProcess(ws, installSuccess, installFailure, NODE_HOME "/npm", + uv::Process::Uid(APP_UID), uv::Process::Gid(APP_GID), + uv::Process::Cwd(EXEC_HOME), + uv::Process::Env("PATH=$PATH:" NODE_HOME), + NODE_HOME "/npm", + "install", "-g", pathname); + } } else if (t == "networkSave") { auto statusFunc = [s = ws.shared_from_this()](wpi::StringRef msg) { diff --git a/deps/tools/configServer/src/resources/frcvision.js b/deps/tools/configServer/src/resources/frcvision.js index 9d101fb23f..2142386396 100644 --- a/deps/tools/configServer/src/resources/frcvision.js +++ b/deps/tools/configServer/src/resources/frcvision.js @@ -107,7 +107,7 @@ var connectedButtonClasses = [ 'cameraCopyConfig', 'cameraKey' ]; -var writableButtonIds = ['networkSave', 'visionSave', 'applicationSave', 'fileUploadButton', 'romiSaveExternalIOConfig']; +var writableButtonIds = ['networkSave', 'visionSave', 'applicationSave', 'fileUploadButton', 'romiSaveExternalIOConfig', 'romiServiceUploadButton']; var systemStatusIds = ['systemMemoryFree1s', 'systemMemoryFree5s', 'systemMemoryAvail1s', 'systemMemoryAvail5s', 'systemCpuUser1s', 'systemCpuUser5s', @@ -313,6 +313,13 @@ function connect() { updateRomiRobotPorts(); } break; + case 'romiServiceUploadComplete': + $('#romiServiceUploadButton').button('reset'); + updateRomiServiceUploadView(); + if (msg.success) { + displaySuccess('Romi WebService successfully uploaded!'); + } + break; case 'networkSettings': $('#networkApproach').val(msg.networkApproach); $('#networkAddress').val(msg.networkAddress); @@ -1232,6 +1239,58 @@ $('#fileUploadButton').click(function() { uploadFile(0); }); +// Romi Service upload +var romiServiceUploadFiles = []; + +function updateRomiServiceUploadView() { + $('#romiServiceUploadFile').val(null); + fileUploadFiles = []; +} + +$('#romiServiceUploadFile').change(function() { + romiServiceUploadFiles = this.files; + dismissStatus(); +}); + +$('#romiServiceUploadButton').click(function() { + if (romiServiceUploadFiles.length <= 0) { + return; + } + + $('#romiServiceUploadButton').button('loading'); + + var msg = { + type: 'romiServiceStartUpload', + }; + connection.send(JSON.stringify(msg)); + + var reader = new FileReader(); + var file = romiServiceUploadFiles.item(0); + + function uploadFile(start) { + var nextSlice = start + (64 * 1024) + 1; + reader.onloadend = function(e) { + if (e.target.readyState !== FileReader.DONE) { + return; + } + connection.send(e.target.result); + if (nextSlice < file.size) { + // more to go + uploadFile(nextSlice); + } else { + // done + var msg = { + type: 'romiServiceFinishUpload', + fileName: file.name + }; + connection.send(JSON.stringify(msg)); + } + } + reader.readAsArrayBuffer(file.slice(start, nextSlice)); + } + uploadFile(0); +}); + // Start with display disconnected and start initial connection attempt displayDisconnected(); @@ -1241,4 +1300,5 @@ updateWifiModeView(); updateVisionSettingsView(); updateApplicationView(); updateFileUploadView(); +updateRomiServiceUploadView(); connect(); diff --git a/deps/tools/configServer/src/resources/index.html b/deps/tools/configServer/src/resources/index.html index b9ffff646d..c38157e75a 100644 --- a/deps/tools/configServer/src/resources/index.html +++ b/deps/tools/configServer/src/resources/index.html @@ -195,6 +195,31 @@
Romi Status
+
+
+
+
+
Web Service Update
+
+
+
+
+

+ To perform an offline update of the Romi webservice, obtain an appropriate + version from the GitHub release page, and upload the .tgz file here. +

+
+
+ + +
+ +
+
+