2018-10-08 19:20:28 +00:00
package updater
2018-05-01 23:45:06 +00:00
import (
2019-06-18 16:47:29 +00:00
"context"
2020-04-28 00:55:27 +00:00
"fmt"
2018-05-01 23:45:06 +00:00
"os"
2020-07-08 14:42:04 +00:00
"path/filepath"
2018-05-01 23:45:06 +00:00
"runtime"
2023-04-06 14:32:41 +00:00
"strings"
2018-05-01 23:45:06 +00:00
"time"
"github.com/facebookgo/grace/gracenet"
2021-02-28 23:24:38 +00:00
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
2023-07-14 23:41:52 +00:00
"golang.org/x/term"
2021-03-08 16:46:23 +00:00
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
2018-05-01 23:45:06 +00:00
)
const (
2020-07-08 14:42:04 +00:00
DefaultCheckUpdateFreq = time . Hour * 24
2024-03-27 18:06:12 +00:00
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configure-tunnels/local-management/as-a-service/"
2020-07-08 14:42:04 +00:00
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
isManagedInstallFile = ".installedFromPackageManager"
2020-08-12 16:16:14 +00:00
UpdateURL = "https://update.argotunnel.com"
StagingUpdateURL = "https://staging-update.argotunnel.com"
2020-12-28 18:10:01 +00:00
LogFieldVersion = "version"
2018-05-01 23:45:06 +00:00
)
2018-10-08 19:20:28 +00:00
var (
2022-03-08 00:05:48 +00:00
version string
BuiltForPackageManager = ""
2018-10-08 19:20:28 +00:00
)
2018-05-01 23:45:06 +00:00
2020-04-28 00:55:27 +00:00
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
2020-08-05 10:49:53 +00:00
// https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder
2020-04-28 00:55:27 +00:00
type statusSuccess struct {
newVersion string
}
func ( u * statusSuccess ) Error ( ) string {
return fmt . Sprintf ( "cloudflared has been updated to version %s" , u . newVersion )
}
func ( u * statusSuccess ) ExitCode ( ) int {
return 11
}
// UpdateErr implements ExitCoder interface, the app will exit with status code 10
type statusErr struct {
err error
}
func ( e * statusErr ) Error ( ) string {
return fmt . Sprintf ( "failed to update cloudflared: %v" , e . err )
}
func ( e * statusErr ) ExitCode ( ) int {
return 10
}
2020-08-12 16:16:14 +00:00
type updateOptions struct {
2021-02-28 23:24:38 +00:00
updateDisabled bool
isBeta bool
isStaging bool
isForced bool
intendedVersion string
2020-08-12 16:16:14 +00:00
}
2019-02-14 22:26:33 +00:00
type UpdateOutcome struct {
2021-02-28 23:24:38 +00:00
Updated bool
Version string
UserMessage string
Error error
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
func ( uo * UpdateOutcome ) noUpdate ( ) bool {
2020-06-16 22:20:09 +00:00
return uo . Error == nil && uo . Updated == false
2019-02-14 22:26:33 +00:00
}
2020-08-12 16:16:14 +00:00
func Init ( v string ) {
version = v
}
2021-02-28 23:24:38 +00:00
func CheckForUpdate ( options updateOptions ) ( CheckResult , error ) {
2020-08-12 16:16:14 +00:00
cfdPath , err := os . Executable ( )
if err != nil {
2021-02-28 23:24:38 +00:00
return nil , err
2018-05-01 23:45:06 +00:00
}
2020-08-12 16:16:14 +00:00
url := UpdateURL
if options . isStaging {
url = StagingUpdateURL
}
2023-04-06 14:32:41 +00:00
if runtime . GOOS == "windows" {
cfdPath = encodeWindowsPath ( cfdPath )
}
2020-08-12 16:16:14 +00:00
s := NewWorkersService ( version , url , cfdPath , Options { IsBeta : options . isBeta ,
2021-02-28 23:24:38 +00:00
IsForced : options . isForced , RequestedVersion : options . intendedVersion } )
2020-08-12 16:16:14 +00:00
2021-02-28 23:24:38 +00:00
return s . Check ( )
}
2023-04-06 14:32:41 +00:00
func encodeWindowsPath ( path string ) string {
// We do this because Windows allows spaces in directories such as
// Program Files but does not allow these directories to be spaced in batch files.
targetPath := strings . Replace ( path , "Program Files (x86)" , "PROGRA~2" , - 1 )
// This is to do the same in 32 bit systems. We do this second so that the first
// replace is for x86 dirs.
targetPath = strings . Replace ( targetPath , "Program Files" , "PROGRA~1" , - 1 )
return targetPath
}
2018-05-01 23:45:06 +00:00
2021-02-28 23:24:38 +00:00
func applyUpdate ( options updateOptions , update CheckResult ) UpdateOutcome {
if update . Version ( ) == "" || options . updateDisabled {
return UpdateOutcome { UserMessage : update . UserMessage ( ) }
2020-08-12 16:16:14 +00:00
}
2021-02-28 23:24:38 +00:00
err := update . Apply ( )
2018-05-01 23:45:06 +00:00
if err != nil {
2019-02-14 22:26:33 +00:00
return UpdateOutcome { Error : err }
2018-05-01 23:45:06 +00:00
}
2021-02-28 23:24:38 +00:00
return UpdateOutcome { Updated : true , Version : update . Version ( ) , UserMessage : update . UserMessage ( ) }
2018-05-01 23:45:06 +00:00
}
2020-08-12 16:16:14 +00:00
// Update is the handler for the update command from the command line
func Update ( c * cli . Context ) error {
2020-11-25 06:55:13 +00:00
log := logger . CreateLoggerFromContext ( c , logger . EnableTerminalLog )
2020-04-29 20:51:32 +00:00
2020-07-08 14:42:04 +00:00
if wasInstalledFromPackageManager ( ) {
2022-03-08 00:05:48 +00:00
packageManagerName := "a package manager"
if BuiltForPackageManager != "" {
packageManagerName = BuiltForPackageManager
}
log . Error ( ) . Msg ( fmt . Sprintf ( "cloudflared was installed by %s. Please update using the same method." , packageManagerName ) )
2020-07-08 14:42:04 +00:00
return nil
}
2020-08-12 16:16:14 +00:00
isBeta := c . Bool ( "beta" )
if isBeta {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( "cloudflared is set to update to the latest beta version" )
2020-08-12 16:16:14 +00:00
}
isStaging := c . Bool ( "staging" )
if isStaging {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( "cloudflared is set to update from staging" )
2020-08-12 16:16:14 +00:00
}
isForced := c . Bool ( "force" )
if isForced {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( "cloudflared is set to upgrade to the latest publish version regardless of the current version" )
2020-08-12 16:16:14 +00:00
}
2021-02-28 23:24:38 +00:00
updateOutcome := loggedUpdate ( log , updateOptions {
updateDisabled : false ,
isBeta : isBeta ,
isStaging : isStaging ,
isForced : isForced ,
intendedVersion : c . String ( "version" ) ,
} )
2019-02-14 22:26:33 +00:00
if updateOutcome . Error != nil {
2020-04-28 00:55:27 +00:00
return & statusErr { updateOutcome . Error }
2019-02-14 22:26:33 +00:00
}
if updateOutcome . noUpdate ( ) {
2020-12-28 18:10:01 +00:00
log . Info ( ) . Str ( LogFieldVersion , updateOutcome . Version ) . Msg ( "cloudflared is up to date" )
2020-04-28 00:55:27 +00:00
return nil
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
2020-04-28 00:55:27 +00:00
return & statusSuccess { newVersion : updateOutcome . Version }
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
// Checks for an update and applies it if one is available
2020-11-25 06:55:13 +00:00
func loggedUpdate ( log * zerolog . Logger , options updateOptions ) UpdateOutcome {
2021-02-28 23:24:38 +00:00
checkResult , err := CheckForUpdate ( options )
if err != nil {
log . Err ( err ) . Msg ( "update check failed" )
return UpdateOutcome { Error : err }
}
updateOutcome := applyUpdate ( options , checkResult )
2019-02-14 22:26:33 +00:00
if updateOutcome . Updated {
2020-12-28 18:10:01 +00:00
log . Info ( ) . Str ( LogFieldVersion , updateOutcome . Version ) . Msg ( "cloudflared has been updated" )
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
if updateOutcome . Error != nil {
2021-02-28 23:24:38 +00:00
log . Err ( updateOutcome . Error ) . Msg ( "update failed to apply" )
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
return updateOutcome
2018-05-01 23:45:06 +00:00
}
2019-06-18 16:47:29 +00:00
// AutoUpdater periodically checks for new version of cloudflared.
type AutoUpdater struct {
configurable * configurable
listeners * gracenet . Net
updateConfigChan chan * configurable
2020-11-25 06:55:13 +00:00
log * zerolog . Logger
2019-06-18 16:47:29 +00:00
}
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
type configurable struct {
enabled bool
freq time . Duration
}
2021-02-28 23:24:38 +00:00
func NewAutoUpdater ( updateDisabled bool , freq time . Duration , listeners * gracenet . Net , log * zerolog . Logger ) * AutoUpdater {
2019-06-18 16:47:29 +00:00
return & AutoUpdater {
2021-02-28 23:24:38 +00:00
configurable : createUpdateConfig ( updateDisabled , freq , log ) ,
2019-06-18 16:47:29 +00:00
listeners : listeners ,
updateConfigChan : make ( chan * configurable ) ,
2020-11-25 06:55:13 +00:00
log : log ,
2019-06-18 16:47:29 +00:00
}
}
2021-02-28 23:24:38 +00:00
func createUpdateConfig ( updateDisabled bool , freq time . Duration , log * zerolog . Logger ) * configurable {
if isAutoupdateEnabled ( log , updateDisabled , freq ) {
log . Info ( ) . Dur ( "autoupdateFreq" , freq ) . Msg ( "Autoupdate frequency is set" )
return & configurable {
enabled : true ,
freq : freq ,
}
} else {
return & configurable {
enabled : false ,
freq : DefaultCheckUpdateFreq ,
}
}
}
2019-06-18 16:47:29 +00:00
func ( a * AutoUpdater ) Run ( ctx context . Context ) error {
ticker := time . NewTicker ( a . configurable . freq )
for {
2021-02-28 23:24:38 +00:00
updateOutcome := loggedUpdate ( a . log , updateOptions { updateDisabled : ! a . configurable . enabled } )
if updateOutcome . Updated {
Init ( updateOutcome . Version )
if IsSysV ( ) {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
a . log . Info ( ) . Msg ( "Restarting service managed by SysV..." )
pid , err := a . listeners . StartProcess ( )
if err != nil {
a . log . Err ( err ) . Msg ( "Unable to restart server automatically" )
return & statusErr { err : err }
2019-06-18 16:47:29 +00:00
}
2021-02-28 23:24:38 +00:00
// stop old process after autoupdate. Otherwise we create a new process
// after each update
a . log . Info ( ) . Msgf ( "PID of the new process is %d" , pid )
2019-06-18 16:47:29 +00:00
}
2021-02-28 23:24:38 +00:00
return & statusSuccess { newVersion : updateOutcome . Version }
} else if updateOutcome . UserMessage != "" {
a . log . Warn ( ) . Msg ( updateOutcome . UserMessage )
2019-06-18 16:47:29 +00:00
}
2021-02-28 23:24:38 +00:00
2019-06-18 16:47:29 +00:00
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
case newConfigurable := <- a . updateConfigChan :
ticker . Stop ( )
a . configurable = newConfigurable
ticker = time . NewTicker ( a . configurable . freq )
// Check if there is new version of cloudflared after receiving new AutoUpdaterConfigurable
case <- ticker . C :
}
}
}
// Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently
2021-02-28 23:24:38 +00:00
func ( a * AutoUpdater ) Update ( updateDisabled bool , newFreq time . Duration ) {
a . updateConfigChan <- createUpdateConfig ( updateDisabled , newFreq , a . log )
2019-06-18 16:47:29 +00:00
}
2021-02-28 23:24:38 +00:00
func isAutoupdateEnabled ( log * zerolog . Logger , updateDisabled bool , updateFreq time . Duration ) bool {
if ! supportAutoUpdate ( log ) {
2019-06-18 16:47:29 +00:00
return false
}
2021-02-28 23:24:38 +00:00
return ! updateDisabled && updateFreq != 0
2019-06-18 16:47:29 +00:00
}
2021-02-28 23:24:38 +00:00
func supportAutoUpdate ( log * zerolog . Logger ) bool {
2018-05-01 23:45:06 +00:00
if runtime . GOOS == "windows" {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( noUpdateOnWindowsMessage )
2018-05-01 23:45:06 +00:00
return false
}
2020-07-08 14:42:04 +00:00
if wasInstalledFromPackageManager ( ) {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( noUpdateManagedPackageMessage )
2020-07-08 14:42:04 +00:00
return false
}
2018-05-01 23:45:06 +00:00
if isRunningFromTerminal ( ) {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( noUpdateInShellMessage )
2018-05-01 23:45:06 +00:00
return false
}
2019-06-18 16:47:29 +00:00
return true
2018-05-01 23:45:06 +00:00
}
2020-07-08 14:42:04 +00:00
func wasInstalledFromPackageManager ( ) bool {
ok , _ := config . FileExists ( filepath . Join ( config . DefaultUnixConfigLocation , isManagedInstallFile ) )
2022-03-08 00:05:48 +00:00
return len ( BuiltForPackageManager ) != 0 || ok
2020-07-08 14:42:04 +00:00
}
2018-05-01 23:45:06 +00:00
func isRunningFromTerminal ( ) bool {
2023-07-14 23:41:52 +00:00
return term . IsTerminal ( int ( os . Stdout . Fd ( ) ) )
2018-05-01 23:45:06 +00:00
}
2020-04-28 00:55:27 +00:00
func IsSysV ( ) bool {
if runtime . GOOS != "linux" {
return false
}
if _ , err := os . Stat ( "/run/systemd/system" ) ; err == nil {
return false
}
return true
}