/*----------------------------------------------------------------------------*/ /* Copyright (c) 2018 FIRST. All Rights Reserved. */ /* Open Source Software - may be modified and shared by FRC teams. The code */ /* must be accompanied by the FIRST BSD license file in the root directory of */ /* the project. */ /*----------------------------------------------------------------------------*/ #include "WebSocketHandlers.h" #include #include #include #include #include #include #include #include #include #include #include #include "Application.h" #include "NetworkSettings.h" #include "SystemStatus.h" #include "VisionSettings.h" #include "VisionStatus.h" namespace uv = wpi::uv; #define SERVICE "/service/camera" struct WebSocketData { ~WebSocketData() { if (uploadFd != -1) ::close(uploadFd); } bool visionLogEnabled = false; int uploadFd = -1; bool uploadText = false; char uploadFilename[128]; wpi::sig::ScopedConnection sysStatusConn; wpi::sig::ScopedConnection sysWritableConn; wpi::sig::ScopedConnection visStatusConn; wpi::sig::ScopedConnection visLogConn; wpi::sig::ScopedConnection netSettingsConn; wpi::sig::ScopedConnection visSettingsConn; wpi::sig::ScopedConnection appSettingsConn; }; static void SendWsText(wpi::WebSocket& ws, const wpi::json& j) { wpi::SmallVector toSend; wpi::raw_uv_ostream os{toSend, 4096}; os << j; ws.SendText(toSend, [](wpi::MutableArrayRef bufs, uv::Error) { for (auto&& buf : bufs) buf.Deallocate(); }); } template static void RunProcess(wpi::WebSocket& ws, OnSuccessFunc success, OnFailFunc fail, const wpi::Twine& file, const Args&... args) { uv::Loop& loop = ws.GetStream().GetLoopRef(); // create pipe to capture stderr auto pipe = uv::Pipe::Create(loop); if (auto proc = uv::Process::Spawn( loop, file, pipe ? uv::Process::StdioCreatePipe(2, *pipe, UV_WRITABLE_PIPE) : uv::Process::Option(), args...)) { // capture stderr output into string auto output = std::make_shared(); if (pipe) { pipe->StartRead(); pipe->data.connect([output](uv::Buffer& buf, size_t len) { output->append(buf.base, len); }); pipe->end.connect([p = pipe.get()] { p->Close(); }); } // on exit, report proc->exited.connect( [ p = proc.get(), output, s = ws.shared_from_this(), fail, success ]( int64_t status, int sig) { if (status != EXIT_SUCCESS) { SendWsText( *s, {{"type", "status"}, {"code", status}, {"message", *output}}); fail(*s); } else { success(*s); } p->Close(); }); } else { SendWsText(ws, {{"type", "status"}, {"message", "could not spawn process"}}); fail(ws); } } void InitWs(wpi::WebSocket& ws) { // set ws data auto data = std::make_shared(); ws.SetData(data); // send initial system status and hook up system status updates auto sysStatus = SystemStatus::GetInstance(); auto statusFunc = [&ws](const wpi::json& j) { SendWsText(ws, j); }; statusFunc(sysStatus->GetStatusJson()); data->sysStatusConn = sysStatus->status.connect_connection(statusFunc); auto writableFunc = [&ws](bool writable) { if (writable) SendWsText(ws, {{"type", "systemWritable"}}); else SendWsText(ws, {{"type", "systemReadOnly"}}); }; writableFunc(sysStatus->GetWritable()); data->sysWritableConn = sysStatus->writable.connect_connection(writableFunc); // hook up vision status updates and logging auto visStatus = VisionStatus::GetInstance(); data->visStatusConn = visStatus->update.connect_connection( [&ws](const wpi::json& j) { SendWsText(ws, j); }); data->visLogConn = visStatus->log.connect_connection([&ws](const wpi::json& j) { auto d = ws.GetData(); if (d->visionLogEnabled) SendWsText(ws, j); }); visStatus->UpdateStatus(); // send initial network settings auto netSettings = NetworkSettings::GetInstance(); auto netSettingsFunc = [&ws](const wpi::json& j) { SendWsText(ws, j); }; netSettingsFunc(netSettings->GetStatusJson()); data->netSettingsConn = netSettings->status.connect_connection(netSettingsFunc); // send initial vision settings auto visSettings = VisionSettings::GetInstance(); auto visSettingsFunc = [&ws](const wpi::json& j) { SendWsText(ws, j); }; visSettingsFunc(visSettings->GetStatusJson()); data->visSettingsConn = visSettings->status.connect_connection(visSettingsFunc); // send initial application settings auto appSettings = Application::GetInstance(); auto appSettingsFunc = [&ws](const wpi::json& j) { SendWsText(ws, j); }; appSettingsFunc(appSettings->GetStatusJson()); data->appSettingsConn = appSettings->status.connect_connection(appSettingsFunc); } void ProcessWsText(wpi::WebSocket& ws, wpi::StringRef msg) { wpi::errs() << "ws: '" << msg << "'\n"; // parse wpi::json j; try { j = wpi::json::parse(msg, nullptr, false); } catch (const wpi::json::parse_error& e) { wpi::errs() << "parse error at byte " << e.byte << ": " << e.what() << '\n'; return; } // top level must be an object if (!j.is_object()) { wpi::errs() << "not object\n"; return; } // type std::string type; try { type = j.at("type").get(); } catch (const wpi::json::exception& e) { wpi::errs() << "could not read type: " << e.what() << '\n'; return; } wpi::outs() << "type: " << type << '\n'; //uv::Loop& loop = ws.GetStream().GetLoopRef(); wpi::StringRef t(type); if (t.startswith("system")) { wpi::StringRef subType = t.substr(6); auto readOnlyFunc = [](wpi::WebSocket& s) { SendWsText(s, {{"type", "systemReadOnly"}}); }; auto writableFunc = [](wpi::WebSocket& s) { SendWsText(s, {{"type", "systemWritable"}}); }; if (subType == "Restart") { RunProcess(ws, [](wpi::WebSocket&) {}, [](wpi::WebSocket&) {}, "/sbin/reboot", "/sbin/reboot"); } else if (subType == "ReadOnly") { RunProcess( ws, readOnlyFunc, writableFunc, "/bin/sh", "/bin/sh", "-c", "/bin/mount -o remount,ro / && /bin/mount -o remount,ro /boot"); } else if (subType == "Writable") { RunProcess( ws, writableFunc, readOnlyFunc, "/bin/sh", "/bin/sh", "-c", "/bin/mount -o remount,rw / && /bin/mount -o remount,rw /boot"); } } else if (t.startswith("vision")) { wpi::StringRef subType = t.substr(6); auto statusFunc = [s = ws.shared_from_this()](wpi::StringRef msg) { SendWsText(*s, {{"type", "status"}, {"message", msg}}); }; if (subType == "Up") { VisionStatus::GetInstance()->Up(statusFunc); } else if (subType == "Down") { VisionStatus::GetInstance()->Down(statusFunc); } else if (subType == "Term") { VisionStatus::GetInstance()->Terminate(statusFunc); } else if (subType == "Kill") { VisionStatus::GetInstance()->Kill(statusFunc); } else if (subType == "LogEnabled") { try { ws.GetData()->visionLogEnabled = j.at("value").get(); } catch (const wpi::json::exception& e) { wpi::errs() << "could not read visionLogEnabled value: " << e.what() << '\n'; return; } } else if (subType == "Save") { try { VisionSettings::GetInstance()->Set(j.at("settings"), statusFunc); } catch (const wpi::json::exception& e) { wpi::errs() << "could not read visionSave value: " << e.what() << '\n'; return; } } } else if (t == "networkSave") { auto statusFunc = [s = ws.shared_from_this()](wpi::StringRef msg) { SendWsText(*s, {{"type", "status"}, {"message", msg}}); }; try { NetworkSettings::Mode mode; auto& approach = j.at("networkApproach").get_ref(); if (approach == "dhcp") mode = NetworkSettings::kDhcp; else if (approach == "static") mode = NetworkSettings::kStatic; else if (approach == "dhcp-fallback") mode = NetworkSettings::kDhcpStatic; else { wpi::errs() << "could not understand networkApproach value: " << approach << '\n'; return; } NetworkSettings::GetInstance()->Set( mode, j.at("networkAddress").get_ref(), j.at("networkMask").get_ref(), j.at("networkGateway").get_ref(), j.at("networkDNS").get_ref(), statusFunc); } catch (const wpi::json::exception& e) { wpi::errs() << "could not read networkSave value: " << e.what() << '\n'; return; } } else if (t.startswith("application")) { wpi::StringRef subType = t.substr(11); auto statusFunc = [s = ws.shared_from_this()](wpi::StringRef msg) { SendWsText(*s, {{"type", "status"}, {"message", msg}}); }; std::string appType; try { appType = j.at("applicationType").get(); } catch (const wpi::json::exception& e) { wpi::errs() << "could not read applicationSave value: " << e.what() << '\n'; return; } if (subType == "Save") { Application::GetInstance()->Set(appType, statusFunc); } else if (subType == "StartUpload") { auto d = ws.GetData(); std::strcpy(d->uploadFilename, EXEC_HOME "/appUploadXXXXXX"); d->uploadFd = Application::GetInstance()->StartUpload( appType, d->uploadFilename, statusFunc); d->uploadText = wpi::StringRef(appType).endswith("python"); } else if (subType == "FinishUpload") { auto d = ws.GetData(); if (d->uploadFd != -1) Application::GetInstance()->FinishUpload(appType, d->uploadFd, d->uploadFilename, statusFunc); d->uploadFd = -1; SendWsText(ws, {{"type", "applicationSaveComplete"}}); } } } void ProcessWsBinary(wpi::WebSocket& ws, wpi::ArrayRef msg) { auto d = ws.GetData(); if (d->uploadFd != -1) Application::GetInstance()->Upload(d->uploadFd, d->uploadText, msg); }