feat(tailshare): block multiple instances across tabs
This commit is contained in:
@@ -21,8 +21,12 @@ import {
|
||||
getPeers,
|
||||
getWaitingFileCount,
|
||||
} from "@webnet/tsconnect-redux"
|
||||
import { useTailshareDispatch, useTailshareSelector } from "../store/store"
|
||||
import { getIpnPrepareWillBuild, triggerIpnPrepareBuild } from "../store/slices/ipnPrepare"
|
||||
import { useTailshareSelector, useTailshareStore } from "../store/store"
|
||||
import {
|
||||
getIpnPrepareAlreadyRunning,
|
||||
getIpnPrepareWillBuild,
|
||||
triggerIpnBuild,
|
||||
} from "../store/slices/ipnPrepare"
|
||||
import { use, type ReactNode } from "react"
|
||||
import { Link } from "wouter"
|
||||
import { useDisclosure } from "@mantine/hooks"
|
||||
@@ -37,7 +41,12 @@ function TailscaleQuickSettings() {
|
||||
const ipn = use(IpnContext)
|
||||
const theme = useMantineTheme()
|
||||
|
||||
if (!ipn) return null
|
||||
if (!ipn)
|
||||
return (
|
||||
<Button<typeof Link> mt="md" href="/tailscale" component={Link}>
|
||||
Tailscale settings
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -97,6 +106,16 @@ function TailscaleQuickSettings() {
|
||||
)
|
||||
}
|
||||
|
||||
function TailscaleAlreadyRunningStatus() {
|
||||
return (
|
||||
<>
|
||||
Tailscale cannot start
|
||||
<br />
|
||||
Typically, this is caused by another tab running Tailshare
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function TailscaleRunningStatus() {
|
||||
const ipnSelfNode = useIpnSelector(getIpnSelfNode)
|
||||
const exitNode = useIpnSelector(getExitNode)
|
||||
@@ -142,22 +161,27 @@ function TailscaleRunningStatus() {
|
||||
}
|
||||
|
||||
export function TailscaleStateButton() {
|
||||
const dispatch = useTailshareDispatch()
|
||||
const willBuild = useTailshareSelector(getIpnPrepareWillBuild)
|
||||
const alreadyRunning = useTailshareSelector(getIpnPrepareAlreadyRunning)
|
||||
const store = useTailshareStore()
|
||||
const ipnState = useIpnSelector(getIpnState)
|
||||
const ipnLoginUrl = useIpnSelector(getLoginUrl)
|
||||
const [opened, { open, close }] = useDisclosure()
|
||||
|
||||
const running = ipnState === "Running"
|
||||
const needsLogin = ipnState === "NeedsLogin" && !!ipnLoginUrl
|
||||
const building = ipnState === "NoState" && !!willBuild
|
||||
const notStarting = !willBuild
|
||||
const running = !alreadyRunning && ipnState === "Running"
|
||||
const needsLogin = !running && ipnState === "NeedsLogin" && !!ipnLoginUrl
|
||||
const building = !needsLogin && ipnState === "NoState" && !!willBuild
|
||||
const notStarting = !building && !willBuild
|
||||
|
||||
let label: string
|
||||
let tooltip: ReactNode
|
||||
let icon: ReactNode
|
||||
|
||||
if (running) {
|
||||
if (alreadyRunning) {
|
||||
label = "Tailscale cannot start: open status"
|
||||
tooltip = <TailscaleAlreadyRunningStatus />
|
||||
icon = <NetworkXIcon />
|
||||
} else if (running) {
|
||||
label = "Tailscale running: open status"
|
||||
tooltip = <TailscaleRunningStatus />
|
||||
icon = <NetworkIcon />
|
||||
@@ -186,11 +210,11 @@ export function TailscaleStateButton() {
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
if (needsLogin) window.open(ipnLoginUrl)
|
||||
else if (notStarting) dispatch(triggerIpnPrepareBuild())
|
||||
else if (notStarting) triggerIpnBuild(store)
|
||||
else open()
|
||||
}}
|
||||
variant={running || needsLogin ? "filled" : "default"}
|
||||
color={needsLogin ? "red" : undefined}
|
||||
variant={running || needsLogin || alreadyRunning ? "filled" : "default"}
|
||||
color={needsLogin || alreadyRunning ? "red" : undefined}
|
||||
size="xl"
|
||||
aria-label={label}
|
||||
>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { type ReactNode, useEffect, useEffectEvent, useMemo, useRef, useState } from "react"
|
||||
import { initIPN, InMemoryFileOps, IPN, WebStorageState } from "@webnet/tsconnect"
|
||||
import { useClient } from "../hooks/useClient"
|
||||
import { useTailshareDispatch, useTailshareSelector } from "../store/store"
|
||||
import { useTailshareDispatch, useTailshareSelector, useTailshareStore } from "../store/store"
|
||||
import {
|
||||
getIpnPrepareWillBuild,
|
||||
selectIpnPrepareSlice,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
setIpnPrepareControlURL,
|
||||
setIpnPrepareHostname,
|
||||
setIpnPrepareTaildrop,
|
||||
triggerIpnPrepareBuild,
|
||||
triggerIpnBuild,
|
||||
type IpnPrepare,
|
||||
} from "../store/slices/ipnPrepare"
|
||||
import wasmUrl from "@webnet/tsconnect/main.wasm"
|
||||
@@ -136,7 +136,7 @@ function AutoExitNode({ ipn }: { ipn: IPN }) {
|
||||
|
||||
function AutoInit() {
|
||||
const state = useIpnSelector(getIpnState)
|
||||
const dispatch = useTailshareDispatch()
|
||||
const store = useTailshareStore()
|
||||
const [autostart, setAutostart] = useLocalStorage("ipn#auto")
|
||||
|
||||
useEffect(() => {
|
||||
@@ -147,11 +147,11 @@ function AutoInit() {
|
||||
|
||||
useEffect(() => {
|
||||
if (autostart !== "1") return
|
||||
const timeout = window.setTimeout(() => dispatch(triggerIpnPrepareBuild()), 1000)
|
||||
const timeout = window.setTimeout(() => triggerIpnBuild(store), 1000)
|
||||
return () => {
|
||||
window.clearTimeout(timeout)
|
||||
}
|
||||
}, [dispatch, autostart])
|
||||
}, [store, autostart])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Code,
|
||||
CopyButton,
|
||||
Divider,
|
||||
Group,
|
||||
List,
|
||||
Loader,
|
||||
Modal,
|
||||
@@ -16,15 +17,16 @@ import {
|
||||
Tooltip,
|
||||
useMantineTheme,
|
||||
} from "@mantine/core"
|
||||
import { useTailshareDispatch, useTailshareSelector } from "../store/store"
|
||||
import { useTailshareDispatch, useTailshareSelector, useTailshareStore } from "../store/store"
|
||||
import {
|
||||
getIpnPrepareAlreadyRunning,
|
||||
getIpnPrepareWillBuild,
|
||||
selectIpnPrepareSlice,
|
||||
setIpnPrepareAuthKey,
|
||||
setIpnPrepareControlURL,
|
||||
setIpnPrepareHostname,
|
||||
setIpnPrepareTaildrop,
|
||||
triggerIpnPrepareBuild,
|
||||
triggerIpnBuild,
|
||||
} from "../store/slices/ipnPrepare"
|
||||
import {
|
||||
getExitNode,
|
||||
@@ -43,16 +45,22 @@ import clsx from "clsx"
|
||||
import { useLocalStorage } from "../hooks/useLocalStorage"
|
||||
|
||||
function ConnectTailscale() {
|
||||
const dispatch = useTailshareDispatch()
|
||||
const willBuild = useTailshareSelector(getIpnPrepareWillBuild)
|
||||
const alreadyRunning = useTailshareSelector(getIpnPrepareAlreadyRunning)
|
||||
const store = useTailshareStore()
|
||||
|
||||
return (
|
||||
<Box display={"flex"}>
|
||||
<Button disabled={!!willBuild} onClick={() => dispatch(triggerIpnPrepareBuild())}>
|
||||
<Group>
|
||||
<Button disabled={!!willBuild || alreadyRunning} onClick={() => triggerIpnBuild(store)}>
|
||||
Enable Tailscale
|
||||
</Button>
|
||||
{!!willBuild && <Loader ml={"sm"} />}
|
||||
</Box>
|
||||
{!!willBuild && <Loader />}
|
||||
{alreadyRunning && (
|
||||
<Text>
|
||||
Cannot start Tailscale, this is typically caused by another tab running Tailshare
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||
import { createSlice, type PayloadAction, type Store } from "@reduxjs/toolkit"
|
||||
|
||||
export type StateOptions = "disable" | "localStorage" | "sessionStorage"
|
||||
export type TaildropOptions = "disable" | "memory"
|
||||
@@ -14,6 +14,7 @@ export type IpnPrepare = {
|
||||
|
||||
// actually build the IPN
|
||||
willBuild: null | Omit<IpnPrepare, "willBuild">
|
||||
alreadyRunning: boolean
|
||||
}
|
||||
const initialState: IpnPrepare = {
|
||||
taildrop: "memory",
|
||||
@@ -21,6 +22,7 @@ const initialState: IpnPrepare = {
|
||||
controlURL: null,
|
||||
authKey: null,
|
||||
willBuild: null,
|
||||
alreadyRunning: false,
|
||||
}
|
||||
|
||||
export const ipnPrepareSlice = createSlice({
|
||||
@@ -42,6 +44,9 @@ export const ipnPrepareSlice = createSlice({
|
||||
triggerIpnPrepareBuild: (state, _: PayloadAction<void>) => {
|
||||
state.willBuild = { ...state }
|
||||
},
|
||||
setIpnAlreadyRunning: (state, _: PayloadAction<void>) => {
|
||||
state.alreadyRunning = true
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
getIpnPrepareTaildrop: (state) => state.taildrop,
|
||||
@@ -49,6 +54,7 @@ export const ipnPrepareSlice = createSlice({
|
||||
getIpnPreapreControlURL: (state) => state.controlURL,
|
||||
getIpnPrepareAuthKey: (state) => state.authKey,
|
||||
getIpnPrepareWillBuild: (state) => state.willBuild,
|
||||
getIpnPrepareAlreadyRunning: (state) => state.alreadyRunning,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -58,7 +64,6 @@ export const {
|
||||
setIpnPrepareControlURL,
|
||||
setIpnPrepareHostname,
|
||||
setIpnPrepareTaildrop,
|
||||
triggerIpnPrepareBuild,
|
||||
},
|
||||
selectors: {
|
||||
getIpnPreapreControlURL,
|
||||
@@ -66,6 +71,23 @@ export const {
|
||||
getIpnPrepareHostname,
|
||||
getIpnPrepareTaildrop,
|
||||
getIpnPrepareWillBuild,
|
||||
getIpnPrepareAlreadyRunning,
|
||||
},
|
||||
selectSlice: selectIpnPrepareSlice,
|
||||
} = ipnPrepareSlice
|
||||
const {
|
||||
actions: { triggerIpnPrepareBuild, setIpnAlreadyRunning },
|
||||
} = ipnPrepareSlice
|
||||
|
||||
export function triggerIpnBuild(store: Store<{ ipnPrepare: IpnPrepare }>) {
|
||||
const state = store.getState()
|
||||
if (state.ipnPrepare.willBuild) return
|
||||
navigator.locks.request("ipn.triggerBuild", { ifAvailable: true }, (lock) => {
|
||||
if (lock) {
|
||||
store.dispatch(triggerIpnPrepareBuild())
|
||||
return new Promise(() => void 0)
|
||||
} else {
|
||||
store.dispatch(setIpnAlreadyRunning())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user