package controllers import ( "encoding/json" "fmt" "io" "net/http" "strings" "github.com/fiskerinc/cloud-services/services/attendant/services" "github.com/fiskerinc/cloud-services/pkg/cache" "github.com/fiskerinc/cloud-services/pkg/common" s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus" "github.com/fiskerinc/cloud-services/pkg/db/queries" "github.com/fiskerinc/cloud-services/pkg/grpc/sms" "github.com/fiskerinc/cloud-services/pkg/logger" "github.com/fiskerinc/cloud-services/pkg/manifestsender" "github.com/fiskerinc/cloud-services/pkg/redis" vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" "github.com/fiskerinc/cloud-services/pkg/hwversion" "github.com/go-pg/pg/v10" r "github.com/gomodule/redigo/redis" "github.com/pkg/errors" ) const redisObjectExpire = 3600 const ( PackageDownloadStart = "package_download_start" PackageDownloadComplete = "package_download_complete" PackageInstallStart = "package_install_start" PackageInstallComplete = "package_install_complete" InstallError = "install_error" ) var RepeatedStatus = errors.New("RepeatedStatus") // CarUpdateProgress takes in a car update message and saves it to our database // This includes setting the status of a car update, and telling the car and SAP that the update is done func NewCarUpdateProgress(clientPool redis.ClientPoolInterface, ka *services.KeepAwake, db *services.DB, device common.Device) CarUpdateProgressInterface { if device == common.TRex { return &CarUpdateProgress{ RedisClientPool: clientPool, DB: db, ka: ka, } } if device == common.HMI { return &HMICarUpdateProgress{ conf: services.GetVehicleConfig(), sms: services.GetSMSClient(), ka: ka, CarUpdateProgress: CarUpdateProgress{ RedisClientPool: clientPool, DB: db, ka: ka, }, } } return nil } type CarUpdateProgressInterface interface { Process(vin string, data []byte) error ProcessStatus(vin string, status common.CarUpdateProgress) error Dispose() } type CarUpdateProgress struct { RedisClientPool redis.ClientPoolInterface DB *services.DB ka *services.KeepAwake } func (cu *CarUpdateProgress) Process(vin string, data []byte) error { var status common.CarUpdateProgress err := json.Unmarshal(data, &status) if err != nil { return err } return cu.ProcessStatus(vin, status) } func (cu *CarUpdateProgress) ProcessStatus(vin string, status common.CarUpdateProgress) (err error) { if cu.transformDBCarUpdateProgress(&status) { err = cu.logStatusDB(status) // If the error is the repeated, we can just exit early if err != nil { if errors.Is(err, queries.RepeatedStatus) { err = nil } return } } cu.cancelTheCANAwake(vin, status) batch := redis.NewRedisBatchCommands() cu.transformRedisCarUpdateProgress(&status) cu.BatchCacheRedis(batch, redis.CarUpdateStatusHashKey(status.CarUpdateID), &status) client := cu.RedisClientPool.GetFromPool() defer client.Close() _, err = client.ExecuteBatch(batch) if err != nil { return err } // do not send car update status for internal cloud statuses if cu.isInternalStatus(status) { return nil } msg := cu.getMessage(&status) err = cu.publishStatusHMI(vin, &msg) if err != nil { return err } msgMobile := cu.getMessageForMobile(&status, vin) err = cu.publishStatusMobile(vin, &msgMobile) if err != nil { return err } err = cu.onUpdateManifestComplete(&status, vin) return err } // We will try and cancel sending CAN status stuff func (cu *CarUpdateProgress) cancelTheCANAwake(vin string, status common.CarUpdateProgress) { switch status.Status { case s.DownloadFailed, s.InstallFailed, s.ManifestCancelAccepted, s.ManifestCancelRejected, s.ManifestError, s.ManifestRejected, s.ManifestValidationFailed, s.RequirementsFailed, s.ManifestCanceled: logger.Info().Msgf("canceling CAN Awake for %s because %s", vin, status.Status) cu.ka.RemoveKeepAwakeMessage(vin) } } func (cu *CarUpdateProgress) isInternalStatus(status common.CarUpdateProgress) bool { return status.Status == s.Sent || status.Status == s.Pending } func (cu *CarUpdateProgress) transformDBCarUpdateProgress(status *common.CarUpdateProgress) bool { switch status.Status { case s.DownloadStarted: if status.PackageCurrent == 0 { status.Status = PackageDownloadStart } return true case s.DownloadCompleted: if status.PackageCurrent == status.PackageTotal { status.Status = PackageDownloadComplete } return true case s.InstallStarted: if status.InstalledFiles == 0 && status.TotalFiles > 0 { status.Status = PackageInstallStart } return true case s.InstallSucceeded: if status.InstalledFiles == status.TotalFiles && status.TotalFiles > 0 { status.Status = PackageInstallComplete } return true case InstallError: status.Status = s.InstallFailed return true case s.Installing: return false case s.Downloading: // these status updates do not need to be saved in the database return false } return true } func (cu *CarUpdateProgress) transformRedisCarUpdateProgress(status *common.CarUpdateProgress) { switch status.Status { case s.DownloadStarted, s.DownloadCompleted, PackageDownloadStart: status.Status = s.Downloading case s.InstallStarted, s.InstallSucceeded, PackageInstallStart: status.Status = s.Installing } } func (cu *CarUpdateProgress) logStatusDB(status common.CarUpdateProgress) (err error) { // If we are one of these status's we want to ignore, then we need to do some extra database steps, otherwise insert normally carUpdate := common.CarUpdate{ ID: status.CarUpdateID, Status: status.Status, ErrorCode: status.ErrorCode, Info: strings.TrimSpace(fmt.Sprintf("%s %s", status.ECU, status.Info)), } if _, ok := s.NoRepeatUpdateStatus[status.Status]; ok { _, err = cu.DB.GetCarUpdates().UpdateStatusIfNotRepeat(&carUpdate) return } _, err = cu.DB.GetCarUpdates().UpdateStatus(&carUpdate) return err } func (cu *CarUpdateProgress) GetCache(key string) (*common.CarUpdateProgress, error) { client := cu.RedisClientPool.GetFromPool() defer client.Close() status := common.CarUpdateProgress{} err := client.GetObject(key, &status) return &status, err } func (cu *CarUpdateProgress) BatchCacheRedis(batch *redis.RedisBatchCommands, key string, status *common.CarUpdateProgress) { batch.Add(r.Args{}.Add("HSET").Add(key).AddFlat(status)...) batch.Add("EXPIRE", key, redisObjectExpire) } func (cu *CarUpdateProgress) getMessage(status *common.CarUpdateProgress) common.Message { return common.Message{ Handler: "car_update_status", Data: status, } } func (cu *CarUpdateProgress) getMessageForMobile(status *common.CarUpdateProgress, vin string) common.Message { type mobileData struct { VIN string `json:"vin"` *common.CarUpdateProgress } return common.Message{ Handler: "car_update_status", Data: mobileData{vin, status}, } } func (cu *CarUpdateProgress) publishStatusHMI(vin string, msg *common.Message) error { client := cu.RedisClientPool.GetFromPool() defer client.Close() // redis publish to HMI hmiKey := common.HMI.Key(vin) // Add VIN err := client.SafePublishMessage(hmiKey, msg) return err } func (cu *CarUpdateProgress) publishStatusMobile(vin string, msg *common.Message) error { drivers := cache.NewDriversCache(cu.RedisClientPool, cu.DB.GetCars()) // redis publish to mobile devices driverIDs, err := drivers.RetrieveDriverIDs(vin) if err != nil { return err } // Change thos for loop to isntead create a batch and execute it all at once client := cu.RedisClientPool.GetFromPool() defer client.Close() for _, d := range driverIDs { mobileKey := common.Mobile.Key(d) err = client.SafePublishMessage(mobileKey, msg) if err != nil { return err } } return nil } func (cu *CarUpdateProgress) Dispose() { cu.DB = nil } type HMICarUpdateProgress struct { conf vconfig.ConfigServiceInterface sms sms.SMSServiceClient ka *services.KeepAwake CarUpdateProgress } func (h *HMICarUpdateProgress) Process(vin string, data []byte) error { var status common.CarUpdateProgress err := json.Unmarshal(data, &status) if err != nil { return err } if h.downloadComplete(&status) { // stop calling the sendKeepAwakeMessage h.ka.RemoveKeepAwakeMessage(vin) _, err = h.sendManifestToTRex(vin, &status) if err != nil { return err } h.logStatusDB(common.CarUpdateProgress{ CarUpdateID: status.CarUpdateID, Status: s.Sent, Info: "TBOX", }) } return h.ProcessStatus(vin, status) } func (h *HMICarUpdateProgress) downloadComplete(status *common.CarUpdateProgress) bool { return status.Status == s.DownloadCompleted } func (h *HMICarUpdateProgress) getManifest(status *common.CarUpdateProgress) (*common.UpdateManifest, error) { update := common.CarUpdate{ID: status.CarUpdateID} err := h.DB.GetCarUpdates().Load(&update) if err != nil { return nil, err } update.UpdateManifest.CarUpdateID = status.CarUpdateID return update.UpdateManifest, nil } func (h *HMICarUpdateProgress) sendManifestToTRex(vin string, status *common.CarUpdateProgress) (msgID string, err error) { logger.Info().Msgf("HMICarUpdateProgress sendManifestToTRex car_update_id %d", status.CarUpdateID) manifest, err := h.getManifest(status) if err != nil { return } if !manifest.HasSelfDownload() { logger.Error().Msgf("%s download_completed for non-self-download manifest", vin) return } err = hwversion.SetHWVersion(manifest, vin, services.GetDB().GetCars()) if err != nil { // An error here is very unexpected. The hw versioning should have been confirmed earlier before ICC was updated err = errors.WithStack(err) logger.Err(err).Str("VIN", vin).Int64("UpdateID", status.CarUpdateID).Msg("failed to set hw versions for a manifest after ICC complete update") err = nil } manifest.SortECUs() manifest.FilterCompatibleECUs(vin) // This code is going to be removed by mny other PR so not going to mess with it for now client := h.RedisClientPool.GetFromPool() defer client.Close() trex := manifestsender.NewTBOXManifestSender(client, h.conf, h.DB, h.sms, nil) defer trex.Close() msgID, err = trex.ProcessSoftwareUpdate(vin, manifest, services.GetDB().GetCarConfigData()) return } func (h *HMICarUpdateProgress) GetRedisHashKey(status *common.CarUpdateProgress) string { return redis.CarUpdateStatusHMIHashKey(status.CarUpdateID) } // Car Update Done func (cu *CarUpdateProgress) onUpdateManifestComplete(status *common.CarUpdateProgress, vin string) (err error) { success := false final := false submitSAP := false switch status.Status { case s.ManifestSucceeded: success = true submitSAP = true final = true case s.ManifestCanceled, s.ManifestError, s.ManifestRejected: success = false submitSAP = true final = true case s.DownloadFailed, s.ManifestCancelPending, s.RollbackSucceeded, s.RollbackFailed, s.CleanupSucceeded: final = true default: return nil } carUpdatesDB := cu.DB.GetCarUpdates() carUpdate, err := carUpdatesDB.SelectByID(status.CarUpdateID) if err != nil { err = errors.WithStack(err) return } if carUpdate != nil { // Notify car user of in progress update through FOA API fs := services.GetFoaService() foaResp, err := fs.OtaUpdateStatus(vin, carUpdate, status) if err != nil || (foaResp != nil && foaResp.StatusCode != http.StatusOK) { bodyBytes, _ := io.ReadAll(foaResp.Body) bodyString := string(bodyBytes) logger.Err(err).Msgf("notify FOA for update manifest %d final state %s for %s failed with http status %d and message %s", carUpdate.UpdateManifestID, status.Status, vin, foaResp.StatusCode, bodyString) err = nil } } logger.Info().Msgf("Manifest update completed for %s with status of %s", vin, status.Status) if submitSAP { logger.Info().Msg("SAP: No Longer Submit Updates") // sap := services.GetSapService() // err = sap.SubmitResult(vin, success) // if err != nil { // requestBody := struct { // VIN string // Success bool // CarUpdateProgress common.CarUpdateProgress // }{VIN: vin, Success: success, CarUpdateProgress: *status} // logger.Err(err).Interface("body", requestBody).Msgf("failed to call sap submit result") // err = nil // } } if success { // If we are successful, we want to possibly update the cars sums version // Need to pull the manifest to check it has a sums version, and then update the car err = cu.updateCarsSUMSVersion(status) if err != nil { logger.Err(err).Msgf("failed to update car sums version for manifest with CarUpdateID %d", status.CarUpdateID) err = nil } // Send the read_ecu_versions remote command so that the ECU data is updated in postgres ASAP client := services.RedisClientPool().GetFromPool() defer client.Close() err = client.SafePublishMessage( common.TRex.Key(vin), common.Message{ Handler: "read_ecu_versions", Data: common.RemoteReadVersionsCommandArgs{ ECUName: "*", }, }, ) if err != nil { logger.Err(err).Msgf("failed to send read_ecu_versions command to vin %s", vin) err = nil } } if final { // if the manifest is in a final state // then delete the redundant requirements_await rows from car_update_statuses, to avoid overcrowding the table err = cu.truncateRequirementsAwaitForUpdate(status) if err != nil { logger.Err(err).Msgf("failed to delete redundant requirements_await rows from car_update_statuses for manifest with CarUpdateID %d", status.CarUpdateID) err = nil } } return err } func (cu *CarUpdateProgress) truncateRequirementsAwaitForUpdate(status *common.CarUpdateProgress) error { logger.Info().Msgf("Manifest with CarUpdateID %d successful with status %s. Deleting redundant requirements_await rows from car_update_statuses", status.CarUpdateID, status.Status) _, err := cu.DB.GetCarUpdates().TruncateRequirementsAwaitForUpdate(status.CarUpdateID) if err != nil && !errors.Is(err, pg.ErrNoRows) { return err } return nil } // Find the car update, and it gives the update manifest // If the manifest has a sums version, apply it to the car func (cu *CarUpdateProgress) updateCarsSUMSVersion(status *common.CarUpdateProgress) (err error) { carUpdatesDB := cu.DB.GetCarUpdates() carUpdate, err := carUpdatesDB.SelectByID(status.CarUpdateID) if err != nil { err = errors.WithStack(err) return } if carUpdate.UpdateManifest == nil { err = errors.New("failed to pull car updates update manifest") return } um := carUpdate.UpdateManifest // So if we have have a sums version we want to update if um.SUMS == "" { return } carsDB := cu.DB.GetCars() filter := common.Car{ VIN: carUpdate.VIN, } cars, err := carsDB.Select(&filter, nil) if err != nil { err = errors.WithStack(err) return } if len(cars) != 1 { err = fmt.Errorf("did not receive only one car, received: %d", len(cars)) err = errors.WithStack(err) return } car := cars[0] car.SUMSVersion = um.SUMS _, err = carsDB.Update(&car) if err != nil { err = errors.WithStack(err) } return }