package update import ( "errors" "strconv" "strings" ) var ( ErrNeedsLinux = errors.New("Browser updates need Linux with systemd.") ErrNeedsSystemd = errors.New("Browser updates need systemd, which this machine is not running.") ErrNeedsRoot = errors.New("Run this command as root, for example with sudo.") ErrServiceNotFound = errors.New("Could not find the RenewCMS service. Pass --service, --binary and --data-dir.") ErrDataDirNotFound = errors.New("Could not find the data directory. Pass --data-dir.") ErrInvalidPort = errors.New("The port must be a number between 1 and 65535.") ) const ( OSLinux = "linux" pathUnitDescription = "Watch for a RenewCMS restart request" ) type ServiceDescription struct { Name string BinaryPath string DataDir string EnvironmentFile string } type InstallOptions struct { Service string Binary string DataDir string Port string } type MachineProbe interface { OS() string SystemdRunning() bool IsRoot() bool } type ServiceLocator interface { Locate(name string) (ServiceDescription, bool) PortFromEnvironmentFile(path string) (string, bool) } type UnitWriter interface { Write(name string, content string) error Remove(name string) (bool, error) } type UnitManager interface { DaemonReload() error EnableNow(unit string) error DisableNow(unit string) error } type HelperStatusWriter interface { Write(dataDir string, status HelperStatus) error Remove(dataDir string) (bool, error) } func CheckInstallEnvironment(probe MachineProbe) error { if probe.OS() != OSLinux { return ErrNeedsLinux } if !probe.SystemdRunning() { return ErrNeedsSystemd } if !probe.IsRoot() { return ErrNeedsRoot } return nil } func ValidatePort(port string) error { number, err := strconv.Atoi(port) if err != nil || number < 1 || number > 65535 { return ErrInvalidPort } return nil } func RenderPathUnit(layout HelperLayout) string { lines := []string{ "[Unit]", "Description=" + pathUnitDescription, "", "[Path]", "PathExists=" + layout.RestartRequestPath(), "Unit=" + ServiceUnitName, "", "[Install]", "WantedBy=multi-user.target", "", } return strings.Join(lines, "\n") } func RenderServiceUnit(options InstallOptions) string { command := []string{ quoteUnitArgument(options.Binary), "apply-update", "--service", quoteUnitArgument(options.Service), "--binary", quoteUnitArgument(options.Binary), "--data-dir", quoteUnitArgument(options.DataDir), "--port", options.Port, } lines := []string{ "[Unit]", "Description=RenewCMS update helper", "", "[Service]", "Type=oneshot", "User=root", "ExecStart=" + strings.Join(command, " "), "", } return strings.Join(lines, "\n") } var unitArgumentEscaper = strings.NewReplacer( `\`, `\\`, `"`, `\"`, `%`, `%%`, `$`, `$$`, "\n", `\n`, "\r", `\r`, "\t", `\t`, ) func quoteUnitArgument(value string) string { if !strings.ContainsAny(value, " \t\r\n\"'\\%$;") { return value } return `"` + unitArgumentEscaper.Replace(value) + `"` }