client/{tailscale,web}: add initial webUI frontend for self-updates (#10191)
Updates #10187. Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { apiFetch, setUnraidCsrfToken } from "src/api"
|
||||
import { VersionInfo } from "src/hooks/self-update"
|
||||
|
||||
export type NodeData = {
|
||||
Profile: UserProfile
|
||||
@ -20,6 +21,7 @@ export type NodeData = {
|
||||
IsUnraid: boolean
|
||||
UnraidToken: string
|
||||
IPNVersion: string
|
||||
ClientVersion?: VersionInfo
|
||||
URLPrefix: string
|
||||
DomainName: string
|
||||
TailnetName: string
|
||||
|
135
client/web/src/hooks/self-update.ts
Normal file
135
client/web/src/hooks/self-update.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { apiFetch } from "src/api"
|
||||
|
||||
// this type is deserialized from tailcfg.ClientVersion,
|
||||
// so it should not include fields not included in that type.
|
||||
export type VersionInfo = {
|
||||
RunningLatest: boolean
|
||||
LatestVersion?: string
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (!cv) {
|
||||
return {
|
||||
updateState: UpdateState.UpToDate,
|
||||
updateLog: "",
|
||||
}
|
||||
}
|
||||
|
||||
const [updateState, setUpdateState] = useState<UpdateState>(
|
||||
cv.RunningLatest ? UpdateState.UpToDate : UpdateState.Available
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
let timer = 0
|
||||
|
||||
function poll() {
|
||||
apiFetch("/local/v0/update/progress", "GET")
|
||||
.then((res) => res.json())
|
||||
.then((res: UpdateProgress[]) => {
|
||||
// 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)
|
||||
timer = 0
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { updateState, updateLog }
|
||||
}
|
Reference in New Issue
Block a user