diff --git a/gosuper/gosuper/api.go b/gosuper/gosuper/api.go index 69ad93b2..edd4b893 100644 --- a/gosuper/gosuper/api.go +++ b/gosuper/gosuper/api.go @@ -34,6 +34,10 @@ type VPNBody struct { Enable bool } +type LogToDisplayBody struct { + Enable bool +} + func jsonResponse(writer http.ResponseWriter, response interface{}, status int) { jsonBody, err := json.Marshal(response) if err != nil { @@ -67,12 +71,6 @@ func parsePurgeBody(request *http.Request) (appId string, err error) { return } -func responseSender(writer http.ResponseWriter) func(interface{}, string, int) { - return func(data interface{}, errorMsg string, statusCode int) { - jsonResponse(writer, APIResponse{data, errorMsg}, statusCode) - } -} - func responseSenders(writer http.ResponseWriter) (sendResponse func(interface{}, string, int), sendError func(error)) { sendResponse = func(data interface{}, errorMsg string, statusCode int) { jsonResponse(writer, APIResponse{data, errorMsg}, statusCode) @@ -221,3 +219,52 @@ func VPNControl(writer http.ResponseWriter, request *http.Request) { log.Printf("%sd\n", actionDescr) sendResponse("OK", "", http.StatusAccepted) } + +//LogToDisplayControl is used to control tty-replacement service status with dbus +func LogToDisplayControl(writer http.ResponseWriter, request *http.Request) { + sendResponse, sendError := responseSenders(writer) + serviceName := "tty-replacement.service" + var body LogToDisplayBody + if err := parseJSONBody(&body, request); err != nil { + log.Println(err) + sendResponse("Error", err.Error(), http.StatusBadRequest) + return + } + + if systemd.Dbus == nil { + sendError(fmt.Errorf("Systemd dbus unavailable, cannot set log to display state.")) + return + } + + if activeState, err := systemd.Dbus.GetUnitProperty(serviceName, "ActiveState"); err != nil { + sendError(fmt.Errorf("Unable to get log to display status: %v", err)) + return + } else { + status := activeState.Value.String() == `"active"` + enable := body.Enable + if status == enable { + // Nothing to do, return Data = false to signal nothing was changed + sendResponse(false, "", http.StatusOK) + return + } else if enable { + if _, err := systemd.Dbus.StartUnit(serviceName, "fail", nil); err != nil { + sendError(fmt.Errorf("Unable to start service: %v", err)) + return + } + if _, _, err = systemd.Dbus.EnableUnitFiles([]string{serviceName}, false, false); err != nil { + sendError(fmt.Errorf("Unable to enable service: %v", err)) + return + } + } else { + if _, err := systemd.Dbus.StopUnit(serviceName, "fail", nil); err != nil { + sendError(fmt.Errorf("Unable to stop service: %v", err)) + return + } + if _, err = systemd.Dbus.DisableUnitFiles([]string{serviceName}, false); err != nil { + sendError(fmt.Errorf("Unable to disable service: %v", err)) + return + } + } + sendResponse(true, "", http.StatusOK) + } +} diff --git a/gosuper/gosuper/main.go b/gosuper/gosuper/main.go index 270a9e25..ab9860a4 100644 --- a/gosuper/gosuper/main.go +++ b/gosuper/gosuper/main.go @@ -25,6 +25,7 @@ func setupApi(router *mux.Router) { apiv1.HandleFunc("/reboot", RebootHandler).Methods("POST") apiv1.HandleFunc("/shutdown", ShutdownHandler).Methods("POST") apiv1.HandleFunc("/vpncontrol", VPNControl).Methods("POST") + apiv1.HandleFunc("/set-log-to-display", LogToDisplayControl).Methods("POST") } func startApi(listenAddress string, router *mux.Router) { diff --git a/src/application.coffee b/src/application.coffee index fcd65e42..b1ac8817 100644 --- a/src/application.coffee +++ b/src/application.coffee @@ -84,6 +84,19 @@ logSystemEvent = (logType, app, error) -> utils.mixpanelTrack(logType.eventName, {app, error}) return +logMessage = (msg) -> + logger.log(msg, isSystem: true) + utils.mixpanelTrack(msg) + +logSpecialAction = (action, value, success) -> + if success + msg = "Applied config variable #{action} = #{value}" + else + msg = "Applying config variable #{action} = #{value}" + logMessage(msg) + + + application = {} application.kill = kill = (app, updateDB = true) -> @@ -340,12 +353,14 @@ executeSpecialActionsAndHostConfig = (env) -> if env[key]? && specialActionCallback? # This makes the Special Action Envs only trigger their functions once. if !_.has(executedSpecialActionEnvVars, key) or executedSpecialActionEnvVars[key] != env[key] + logSpecialAction(key, env[key]) specialActionCallback(env[key]) executedSpecialActionEnvVars[key] = env[key] + logSpecialAction(key, env[key], true) hostConfigVars = _.pick env, (val, key) -> return _.startsWith(key, device.hostConfigEnvVarPrefix) if !_.isEmpty(hostConfigVars) - device.setHostConfig(hostConfigVars) + device.setHostConfig(hostConfigVars, logMessage) wrapAsError = (err) -> return err if _.isError(err) @@ -460,6 +475,8 @@ getEnvAndFormatRemoteApps = (deviceId, remoteApps, uuid, apiKey) -> utils.extendEnvVars(app.environment_variable, uuid) .then (fullEnv) -> env = _.omit(fullEnv, _.keys(specialActionEnvVars)) + env = _.omitBy env, (v, k) -> + _.startsWith(k, device.hostConfigEnvVarPrefix) return [ { appId: '' + app.id @@ -528,7 +545,7 @@ application.update = update = (force) -> Promise.map appsWithChangedEnvs, (appId) -> Promise.using lockUpdates(remoteApps[appId], force), -> executeSpecialActionsAndHostConfig(remoteAppEnvs[appId]) - .then -> + .tap -> # If an env var shouldn't cause a restart but requires an action, we should still # save the new env to the DB if !_.includes(toBeUpdated, appId) and !_.includes(toBeInstalled, appId) @@ -538,6 +555,8 @@ application.update = update = (force) -> throw new Error('App not found') app.env = JSON.stringify(remoteAppEnvs[appId]) knex('app').update(app).where({ appId }) + .then (needsReboot) -> + device.reboot() if needsReboot .catch (err) -> logSystemEvent(logTypes.updateAppError, remoteApps[appId], err) .return(allAppIds) diff --git a/src/device.coffee b/src/device.coffee index a98626a1..0231c240 100644 --- a/src/device.coffee +++ b/src/device.coffee @@ -35,7 +35,7 @@ exports.getID = do -> throw new Error('Could not find this device?!') return devices[0].id -rebootDevice = -> +exports.reboot = rebootDevice = -> request.postAsync(config.gosuperAddress + '/v1/reboot') exports.hostConfigEnvVarPrefix = hostConfigEnvVarPrefix = 'RESIN_HOST_' @@ -70,26 +70,30 @@ parseBootConfigFromEnv = (env) -> parsedEnv = _.omit(parsedEnv, forbiddenConfigKeys) return parsedEnv -exports.setHostConfig = (env) -> - Promise.join setBootConfig(env), setLogToDisplay(env), (bootConfigApplied, logToDisplayChanged) -> - rebootDevice() if bootConfigApplied or logToDisplayChanged +exports.setHostConfig = (env, logMessage) -> + Promise.join setBootConfig(env, logMessage), setLogToDisplay(env, logMessage), (bootConfigApplied, logToDisplayChanged) -> + return true if bootConfigApplied or logToDisplayChanged + return false -setLogToDisplay = (env) -> +setLogToDisplay = (env, logMessage) -> if env['RESIN_HOST_LOG_TO_DISPLAY']? - request.postAsync(config.gosuperAddress + '/v1/set-log-to-display') + enable = env['RESIN_HOST_LOG_TO_DISPLAY'] != '0' + request.postAsync(config.gosuperAddress + '/v1/set-log-to-display', {json: true, body: Enable: enable}) .spread (response, body) -> if response.statusCode != 200 - console.log('Error setting log to display:', body, "Status:", response.statusCode) + logMessage("Error setting log to display: #{body}, Status:, #{response.statusCode}") return false else + if body.Data == true + logMessage("#{enable ? "Enabled" : "Disabled"} logs to display") return body.Data else return false -setBootConfig = (env) -> +setBootConfig = (env, logMessage) -> device.getDeviceType() .then (deviceType) -> - throw new Error('This is not a Raspberry Pi') if !_.startsWith(deviceType, 'raspberry-pi') + throw new Error('This is not a Raspberry Pi') if !_.startsWith(deviceType, 'raspberry') Promise.join parseBootConfigFromEnv(env), fs.readFileAsync(bootConfigPath, 'utf8'), (configFromApp, configTxt ) -> throw new Error('No boot config to change') if _.isEmpty(configFromApp) configFromFS = {} @@ -111,6 +115,8 @@ setBootConfig = (env) -> toBeChanged = _.filter toBeChanged, (key) -> configFromApp[key] != configFromFS[key] throw new Error('Nothing to change') if _.isEmpty(toBeChanged) and _.isEmpty(toBeAdded) + + logMessage("Applying boot config: #{configFromApp}") # We add the keys to be added first so they are out of any filters outputConfig = _.map toBeAdded, (key) -> "#{key}=#{configFromApp[key]}" outputConfig = outputConfig.concat _.map configPositions, (key, index) -> @@ -129,7 +135,11 @@ setBootConfig = (env) -> .then -> execAsync('sync') .then -> + logMessage("Applied boot config: #{configFromApp}") return true + .catch (err) -> + logMessage("Error setting boot config: #{err}") + throw err .catch (err) -> console.log('Will not set boot config: ', err) return false