2023-11-28 13:15:19 -05:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
2023-11-15 16:04:44 -05:00
|
|
|
import { useCallback, useEffect, useState } from "react"
|
|
|
|
import { apiFetch } from "src/api"
|
2023-12-06 00:26:34 -05:00
|
|
|
import { VersionInfo } from "src/types"
|
2023-11-15 16:04:44 -05:00
|
|
|
|
|
|
|
// see ipnstate.UpdateProgress
|
|
|
|
export type UpdateProgress = {
|
|
|
|
status: "UpdateFinished" | "UpdateInProgress" | "UpdateFailed"
|
|
|
|
message: string
|
|
|
|
version: string
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum UpdateState {
|
|
|
|
UpToDate,
|
|
|
|
Available,
|
|
|
|
InProgress,
|
|
|
|
Complete,
|
|
|
|
Failed,
|
|
|
|
}
|
|
|
|
|
|
|
|
// useInstallUpdate initiates and tracks a Tailscale self-update via the LocalAPI,
|
|
|
|
// and returns state messages showing the progress of the update.
|
|
|
|
export function useInstallUpdate(currentVersion: string, cv?: VersionInfo) {
|
|
|
|
const [updateState, setUpdateState] = useState<UpdateState>(
|
2023-11-28 16:31:56 -05:00
|
|
|
cv?.RunningLatest ? UpdateState.UpToDate : UpdateState.Available
|
2023-11-15 16:04:44 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
const [updateLog, setUpdateLog] = useState<string>("")
|
|
|
|
|
|
|
|
const appendUpdateLog = useCallback(
|
|
|
|
(msg: string) => {
|
|
|
|
setUpdateLog(updateLog + msg + "\n")
|
|
|
|
},
|
|
|
|
[updateLog, setUpdateLog]
|
|
|
|
)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (updateState !== UpdateState.Available) {
|
|
|
|
// useEffect cleanup function
|
|
|
|
return () => {}
|
|
|
|
}
|
|
|
|
|
|
|
|
setUpdateState(UpdateState.InProgress)
|
|
|
|
|
|
|
|
apiFetch("/local/v0/update/install", "POST").catch((err) => {
|
|
|
|
console.error(err)
|
|
|
|
setUpdateState(UpdateState.Failed)
|
|
|
|
})
|
|
|
|
|
|
|
|
let tsAwayForPolls = 0
|
|
|
|
let updateMessagesRead = 0
|
|
|
|
|
2023-12-06 12:20:44 -05:00
|
|
|
let timer: NodeJS.Timeout | undefined
|
2023-11-15 16:04:44 -05:00
|
|
|
|
|
|
|
function poll() {
|
2023-12-05 18:03:05 -05:00
|
|
|
apiFetch<UpdateProgress[]>("/local/v0/update/progress", "GET")
|
|
|
|
.then((res) => {
|
2023-11-15 16:04:44 -05:00
|
|
|
// res contains a list of UpdateProgresses that is strictly increasing
|
|
|
|
// in size, so updateMessagesRead keeps track (across calls of poll())
|
|
|
|
// of how many of those we have already read. This is why it is not
|
|
|
|
// initialized to zero here and we don't just use res.forEach()
|
|
|
|
for (; updateMessagesRead < res.length; ++updateMessagesRead) {
|
|
|
|
const up = res[updateMessagesRead]
|
|
|
|
if (up.status === "UpdateFailed") {
|
|
|
|
setUpdateState(UpdateState.Failed)
|
|
|
|
if (up.message) appendUpdateLog("ERROR: " + up.message)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (up.status === "UpdateFinished") {
|
|
|
|
// if update finished and tailscaled did not go away (ie. did not restart),
|
|
|
|
// then the version being the same might not be an error, it might just require
|
|
|
|
// the user to restart Tailscale manually (this is required in some cases in the
|
|
|
|
// clientupdate package).
|
|
|
|
if (up.version === currentVersion && tsAwayForPolls > 0) {
|
|
|
|
setUpdateState(UpdateState.Failed)
|
|
|
|
appendUpdateLog(
|
|
|
|
"ERROR: Update failed, still running Tailscale " + up.version
|
|
|
|
)
|
|
|
|
if (up.message) appendUpdateLog("ERROR: " + up.message)
|
|
|
|
} else {
|
|
|
|
setUpdateState(UpdateState.Complete)
|
|
|
|
if (up.message) appendUpdateLog("INFO: " + up.message)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
setUpdateState(UpdateState.InProgress)
|
|
|
|
if (up.message) appendUpdateLog("INFO: " + up.message)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we have gone through the entire loop without returning out of the function,
|
|
|
|
// the update is still in progress. So we want to poll again for further status
|
|
|
|
// updates.
|
|
|
|
timer = setTimeout(poll, 1000)
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
++tsAwayForPolls
|
|
|
|
if (tsAwayForPolls >= 5 * 60) {
|
|
|
|
setUpdateState(UpdateState.Failed)
|
|
|
|
appendUpdateLog(
|
|
|
|
"ERROR: tailscaled went away but did not come back!"
|
|
|
|
)
|
|
|
|
appendUpdateLog("ERROR: last error received:")
|
|
|
|
appendUpdateLog(err.toString())
|
|
|
|
} else {
|
|
|
|
timer = setTimeout(poll, 1000)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
poll()
|
|
|
|
|
|
|
|
// useEffect cleanup function
|
|
|
|
return () => {
|
|
|
|
if (timer) clearTimeout(timer)
|
2023-12-06 12:20:44 -05:00
|
|
|
timer = undefined
|
2023-11-15 16:04:44 -05:00
|
|
|
}
|
2023-11-28 16:31:56 -05:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2023-11-15 16:04:44 -05:00
|
|
|
}, [])
|
|
|
|
|
2023-11-28 16:31:56 -05:00
|
|
|
return !cv
|
|
|
|
? { updateState: UpdateState.UpToDate, updateLog: "" }
|
|
|
|
: { updateState, updateLog }
|
2023-11-15 16:04:44 -05:00
|
|
|
}
|