bastion 0.62.0
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
// runholder.go — what a second start on Windows is told about the first.
|
||||
//
|
||||
// A bastion that finds its run lock held (runguard_windows.go) used to say
|
||||
// only that one was running, and that if it seemed dead it could be ended in
|
||||
// Task Manager. On a machine where `privatae-bastion ui` then failed, that
|
||||
// was the one thing to try, so a running bastion was killed for a symptom
|
||||
// that was not its. The bastion that holds the lock now writes who it is
|
||||
// beside it, and the refusal names it: its process, its program, how it was
|
||||
// started, and the commands that open, check and restart it.
|
||||
//
|
||||
// And the relaunch after a self-update (relaunch_windows.go) starts the new
|
||||
// binary while the old one still holds the lock, a few milliseconds from
|
||||
// exiting. The new one waits for it (takeRunLock) rather than refusing and
|
||||
// leaving the machine with no bastion at all.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type runHolder struct {
|
||||
PID int `json:"pid"`
|
||||
Program string `json:"program,omitempty"`
|
||||
Started string `json:"started,omitempty"` // RFC3339
|
||||
Service bool `json:"service,omitempty"` // the Windows service
|
||||
Console bool `json:"console,omitempty"` // started from a terminal
|
||||
}
|
||||
|
||||
func runHolderPath(dir string) string { return filepath.Join(dir, "run.holder.json") }
|
||||
|
||||
// writeRunHolder records this process as the lock's holder. Best effort: a
|
||||
// refusal without it is only less specific.
|
||||
func writeRunHolder(dir string, console bool) {
|
||||
h := runHolder{PID: os.Getpid(), Started: time.Now().UTC().Format(time.RFC3339),
|
||||
Service: runningAsService, Console: console}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
h.Program = exe
|
||||
}
|
||||
if raw, err := json.Marshal(h); err == nil {
|
||||
_ = atomicWrite(runHolderPath(dir), raw)
|
||||
}
|
||||
}
|
||||
|
||||
func readRunHolder(dir string) (runHolder, bool) {
|
||||
var h runHolder
|
||||
raw, err := os.ReadFile(runHolderPath(dir))
|
||||
if err != nil || json.Unmarshal(raw, &h) != nil || h.PID <= 0 {
|
||||
return runHolder{}, false
|
||||
}
|
||||
return h, true
|
||||
}
|
||||
|
||||
// secondRunWords is the refusal of a second start, for the terminal and for
|
||||
// the dialog a start with no console shows. known is false
|
||||
// when the running bastion left no record (an older version holds the lock).
|
||||
func secondRunWords(h runHolder, known bool) (console string, dialog uiNotice) {
|
||||
what := "A bastion is already running on this machine"
|
||||
restart := "Get-Process privatae-bastion | Format-List Id, Path, StartTime shows it; " +
|
||||
"Stop-Process -Id <its Id>, then Start-ScheduledTask -TaskName " + winServiceName
|
||||
if known {
|
||||
how := "in the background (its sign-in task, or itself after an update)"
|
||||
switch {
|
||||
case h.Service:
|
||||
how = "as the " + winServiceName + " service"
|
||||
case h.Console:
|
||||
how = "from a terminal"
|
||||
}
|
||||
what += " (process " + strconv.Itoa(h.PID)
|
||||
if h.Program != "" {
|
||||
what += ", " + h.Program
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, h.Started); err == nil {
|
||||
what += ", started " + t.Local().Format("2 Jan 15:04")
|
||||
}
|
||||
what += ", " + how + ")"
|
||||
switch {
|
||||
case h.Service:
|
||||
restart = "Restart-Service " + winServiceName + " (from an Administrator terminal)"
|
||||
default:
|
||||
restart = "Stop-Process -Id " + strconv.Itoa(h.PID) + ", then start it again: Start-ScheduledTask -TaskName " +
|
||||
winServiceName + " (or privatae-bastion run)"
|
||||
}
|
||||
}
|
||||
console = fmt.Sprintf("%s.\n"+
|
||||
" It is the one serving your Hosts, and a second copy would fight it for the same local ports,\n"+
|
||||
" so this one is stopping. Nothing is wrong, and nothing needs ending.\n"+
|
||||
" To open it: privatae-bastion ui\n"+
|
||||
" To check it: privatae-bastion status\n"+
|
||||
" To restart it: %s", what, restart)
|
||||
dialog = uiNotice{
|
||||
Title: "Privatae bastion is already running",
|
||||
Body: what + ".\r\n\r\n" +
|
||||
"It is the one serving your Hosts. A second copy would fight it for the same local ports, so this one " +
|
||||
"is stopping. Nothing is wrong, and nothing needs ending.\r\n\r\n" +
|
||||
"To open it, run: privatae-bastion ui\r\n" +
|
||||
"To restart it: " + restart,
|
||||
}
|
||||
return console, dialog
|
||||
}
|
||||
|
||||
// relaunchOfEnv names, in the environment of a bastion started by its
|
||||
// predecessor's self-update, the predecessor's process id.
|
||||
const relaunchOfEnv = "PRIVATAE_BASTION_RELAUNCH_OF"
|
||||
|
||||
// takeRunLock takes the run lock with try. A bastion relaunched by its
|
||||
// predecessor (replaces > 0) first waits up to patience for that process to
|
||||
// exit (waitExit), then tries again until patience has passed: the lock is
|
||||
// released as the predecessor's handles close, a moment after it exits.
|
||||
func takeRunLock(try func() error, replaces int, waitExit func(pid int, d time.Duration), patience time.Duration,
|
||||
now func() time.Time, sleep func(time.Duration)) error {
|
||||
if replaces <= 0 {
|
||||
return try()
|
||||
}
|
||||
deadline := now().Add(patience)
|
||||
waitExit(replaces, patience)
|
||||
for {
|
||||
err := try()
|
||||
if err == nil || !now().Before(deadline) {
|
||||
return err
|
||||
}
|
||||
sleep(200 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// relaunchOf reads relaunchOfEnv once and clears it, so nothing this process
|
||||
// starts inherits it.
|
||||
func relaunchOf() int {
|
||||
v := os.Getenv(relaunchOfEnv)
|
||||
_ = os.Unsetenv(relaunchOfEnv)
|
||||
pid, err := strconv.Atoi(v)
|
||||
if err != nil || pid <= 0 {
|
||||
return 0
|
||||
}
|
||||
return pid
|
||||
}
|
||||
Reference in New Issue
Block a user