Main tain app state in a struct
- Keep ssh connection open for faster info gathering Basic functionality working \o/
This commit is contained in:
parent
f1add627f7
commit
b832051bbe
@ -20,7 +20,7 @@
|
|||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
vendorSha256 =
|
vendorSha256 =
|
||||||
"sha256-0ff4WkOBW+RKDk6eQCXNMsrCqZ+oNxGLaBmvFS7u5JM=";
|
"sha256-FQsILSY4xC2byrg7bMMTJ/HOuq7hMKIffsDYbfm+h6E=";
|
||||||
proxyVendor = true;
|
proxyVendor = true;
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [ pkg-config ];
|
nativeBuildInputs = with pkgs; [ pkg-config ];
|
||||||
|
347
main.go
347
main.go
@ -1,14 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
@ -19,32 +20,180 @@ import (
|
|||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/agent"
|
|
||||||
"golang.org/x/crypto/ssh/knownhosts"
|
"golang.org/x/crypto/ssh/knownhosts"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var commitCache = make(map[string]string)
|
||||||
statusPKPath string
|
|
||||||
currentCommitHash string
|
|
||||||
)
|
|
||||||
|
|
||||||
type xinStatus struct {
|
type xinStatus struct {
|
||||||
widget.Icon
|
|
||||||
|
|
||||||
debug bool
|
debug bool
|
||||||
tabs *container.AppTabs
|
tabs *container.AppTabs
|
||||||
cards []fyne.CanvasObject
|
cards []fyne.CanvasObject
|
||||||
boundStrings []binding.ExternalString
|
boundStrings []binding.ExternalString
|
||||||
|
boundBools []binding.ExternalBool
|
||||||
log *widget.TextGrid
|
log *widget.TextGrid
|
||||||
|
repoCommitHash string
|
||||||
|
repoCommitMsg string
|
||||||
|
config Config
|
||||||
|
upgradeProgress *widget.ProgressBar
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *xinStatus) prependLog(s string) {
|
type Status struct {
|
||||||
|
card *widget.Card
|
||||||
|
commitMessage string
|
||||||
|
client *ssh.Client
|
||||||
|
clientEstablished bool
|
||||||
|
|
||||||
|
ConfigurationRevision string `json:"configurationRevision"`
|
||||||
|
NeedsRestart bool `json:"needs_restart"`
|
||||||
|
NixosVersion string `json:"nixosVersion"`
|
||||||
|
NixpkgsRevision string `json:"nixpkgsRevision"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int32 `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func trim(b []byte) string {
|
||||||
|
head := bytes.Split(b, []byte("\n"))
|
||||||
|
return string(head[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *xinStatus) uptodate() bool {
|
||||||
|
return x.upgradeProgress.Value == float64(len(x.config.Statuses))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *xinStatus) getCommitInfo(c string) string {
|
||||||
|
if c == "DIRTY" {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
if commitCache[c] != "" {
|
||||||
|
return commitCache[c]
|
||||||
|
}
|
||||||
|
|
||||||
|
msgCmd := exec.Command("git", "log", "--format=%B", "-n", "1", c)
|
||||||
|
msgCmd.Dir = x.config.Repo
|
||||||
|
msg, err := msgCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
x.Log(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
strMsg := trim(msg)
|
||||||
|
commitCache[c] = strMsg
|
||||||
|
return strMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *xinStatus) updateRepoInfo() error {
|
||||||
|
revCmd := exec.Command("git", "rev-parse", "HEAD")
|
||||||
|
revCmd.Dir = x.config.Repo
|
||||||
|
currentRev, err := revCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
x.repoCommitHash = trim(currentRev)
|
||||||
|
|
||||||
|
if commitCache[x.repoCommitHash] != "" {
|
||||||
|
x.repoCommitMsg = commitCache[x.repoCommitHash]
|
||||||
|
} else {
|
||||||
|
x.repoCommitMsg = x.getCommitInfo(x.repoCommitHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *xinStatus) updateHostInfo() error {
|
||||||
|
khFile := path.Clean(path.Join(os.Getenv("HOME"), ".ssh/known_hosts"))
|
||||||
|
hostKeyCB, err := knownhosts.New(khFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse %q: %q", khFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := os.ReadFile(x.config.PrivKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't load key %q: %q", x.config.PrivKeyPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := ssh.ParsePrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse key: %q", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sshConf := &ssh.ClientConfig{
|
||||||
|
User: "root",
|
||||||
|
HostKeyAlgorithms: []string{"ssh-ed25519"},
|
||||||
|
Auth: []ssh.AuthMethod{
|
||||||
|
ssh.PublicKeys(signer),
|
||||||
|
},
|
||||||
|
Timeout: 2 * time.Second,
|
||||||
|
HostKeyCallback: hostKeyCB,
|
||||||
|
}
|
||||||
|
|
||||||
|
upToDateCount := len(x.config.Statuses)
|
||||||
|
for _, s := range x.config.Statuses {
|
||||||
|
var err error
|
||||||
|
ds := fmt.Sprintf("%s:%d", s.Host, s.Port)
|
||||||
|
if !s.clientEstablished {
|
||||||
|
s.client, err = ssh.Dial("tcp", ds, sshConf)
|
||||||
|
if err != nil {
|
||||||
|
s.card.Subtitle = "can't connect"
|
||||||
|
upToDateCount = upToDateCount - 1
|
||||||
|
x.Log(fmt.Sprintf("can't Dial host %q (%q): %q", s.Host, ds, err))
|
||||||
|
s.card.Refresh()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.clientEstablished = true
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := s.client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
x.Log(fmt.Sprintf("can't create session: %q", err))
|
||||||
|
upToDateCount = upToDateCount - 1
|
||||||
|
s.clientEstablished = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
output, err := session.Output("xin-status")
|
||||||
|
if err != nil {
|
||||||
|
x.Log(fmt.Sprintf("can't run command: %q", err))
|
||||||
|
upToDateCount = upToDateCount - 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(output, s)
|
||||||
|
if err != nil {
|
||||||
|
x.Log(err.Error())
|
||||||
|
upToDateCount = upToDateCount - 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ConfigurationRevision != x.repoCommitHash {
|
||||||
|
s.card.Subtitle = fmt.Sprintf("%.8s", s.ConfigurationRevision)
|
||||||
|
upToDateCount = upToDateCount - 1
|
||||||
|
} else {
|
||||||
|
s.card.Subtitle = ""
|
||||||
|
}
|
||||||
|
s.card.Refresh()
|
||||||
|
|
||||||
|
s.commitMessage = x.getCommitInfo(s.ConfigurationRevision)
|
||||||
|
}
|
||||||
|
|
||||||
|
x.upgradeProgress.SetValue(float64(upToDateCount))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *xinStatus) Log(s string) {
|
||||||
|
log.Println(s)
|
||||||
|
/*
|
||||||
text := x.log.Text()
|
text := x.log.Text()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
log.Println(s)
|
||||||
x.log.SetText(strings.Join([]string{
|
x.log.SetText(strings.Join([]string{
|
||||||
fmt.Sprintf("%s: %s", now.Format(time.RFC822), s),
|
fmt.Sprintf("%s: %s", now.Format(time.RFC822), s),
|
||||||
text,
|
text,
|
||||||
}, "\n"))
|
}, "\n"))
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -62,74 +211,6 @@ func (c *Config) Load(file string) error {
|
|||||||
return json.Unmarshal(data, &c)
|
return json.Unmarshal(data, &c)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Status struct {
|
|
||||||
ConfigurationRevision string `json:"configurationRevision"`
|
|
||||||
NeedsRestart bool `json:"needs_restart"`
|
|
||||||
NixosVersion string `json:"nixosVersion"`
|
|
||||||
NixpkgsRevision string `json:"nixpkgsRevision"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
Port int32 `json:"port"`
|
|
||||||
User string `json:"user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Status) DialString() string {
|
|
||||||
return fmt.Sprintf("%s:%d", s.Host, s.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Status) Update() error {
|
|
||||||
khFile := path.Clean(path.Join(os.Getenv("HOME"), ".ssh/known_hosts"))
|
|
||||||
hostKeyCB, err := knownhosts.New(khFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't parse %q: %q", khFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := os.ReadFile(statusPKPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't load key %q: %q", statusPKPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
signer, err := ssh.ParsePrivateKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't parse key: %q", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket := os.Getenv("SSH_AUTH_SOCK")
|
|
||||||
agentConn, err := net.Dial("unix", socket)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't Dial agent: %q, %q", socket, err)
|
|
||||||
}
|
|
||||||
agentClient := agent.NewClient(agentConn)
|
|
||||||
|
|
||||||
sshConf := &ssh.ClientConfig{
|
|
||||||
User: s.User,
|
|
||||||
HostKeyAlgorithms: []string{"ssh-ed25519"},
|
|
||||||
Auth: []ssh.AuthMethod{
|
|
||||||
ssh.PublicKeys(signer),
|
|
||||||
ssh.PublicKeysCallback(agentClient.Signers),
|
|
||||||
},
|
|
||||||
Timeout: 2 * time.Second,
|
|
||||||
HostKeyCallback: hostKeyCB,
|
|
||||||
}
|
|
||||||
conn, err := ssh.Dial("tcp", s.DialString(), sshConf)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't Dial host %q (%q): %q", s.Host, s.DialString(), err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
session, err := conn.NewSession()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't create session: %q", err)
|
|
||||||
}
|
|
||||||
defer session.Close()
|
|
||||||
|
|
||||||
output, err := session.Output("xin-status")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't run command: %q", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Unmarshal(output, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Status) ToTable() *widget.Table {
|
func (s *Status) ToTable() *widget.Table {
|
||||||
t := widget.NewTable(
|
t := widget.NewTable(
|
||||||
func() (int, int) {
|
func() (int, int) {
|
||||||
@ -178,82 +259,109 @@ func (s *Status) ToTable() *widget.Table {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCards(c *Config, stat *xinStatus) fyne.CanvasObject {
|
func buildCards(stat *xinStatus) fyne.CanvasObject {
|
||||||
var cards []fyne.CanvasObject
|
var cards []fyne.CanvasObject
|
||||||
for _, s := range c.Statuses {
|
sort.Slice(stat.config.Statuses, func(i, j int) bool {
|
||||||
boundStr := binding.BindString(&s.ConfigurationRevision)
|
return stat.config.Statuses[i].Host < stat.config.Statuses[j].Host
|
||||||
bsl := widget.NewLabelWithData(boundStr)
|
})
|
||||||
|
for _, s := range stat.config.Statuses {
|
||||||
|
commitBStr := binding.BindString(&s.commitMessage)
|
||||||
|
bsl := widget.NewLabelWithData(commitBStr)
|
||||||
|
|
||||||
stat.boundStrings = append(stat.boundStrings, boundStr)
|
restartBBool := binding.BindBool(&s.NeedsRestart)
|
||||||
|
bbl := widget.NewCheckWithData("Reboot", restartBBool)
|
||||||
|
bbl.Disable()
|
||||||
|
|
||||||
//circle := canvas.NewCircle(theme.SelectionColor())
|
stat.boundStrings = append(stat.boundStrings, commitBStr)
|
||||||
//circle.FillColor = color.RGBA{48, 190, 37, 0}
|
stat.boundBools = append(stat.boundBools, restartBBool)
|
||||||
//circle.StrokeWidth = 30
|
|
||||||
//circle.StrokeColor = theme.TextColor()
|
|
||||||
//if s.ConfigurationRevision == "DIRTY" {
|
|
||||||
// circle.FillColor = theme.ErrorColor()
|
|
||||||
//}
|
|
||||||
//circle.Resize(fyne.NewSize(250, 250))
|
|
||||||
|
|
||||||
//card := widget.NewCard(s.Host, "", container.NewVBox(bsl, circle))
|
card := widget.NewCard(s.Host, "",
|
||||||
card := widget.NewCard(s.Host, "", container.NewVBox(bsl))
|
container.NewVBox(
|
||||||
cards = append(cards, card)
|
container.NewHBox(bbl),
|
||||||
}
|
container.NewHBox(bsl),
|
||||||
stat.cards = cards
|
),
|
||||||
return container.NewVBox(
|
|
||||||
widget.NewCard("Some commit message", "somehash", nil),
|
|
||||||
container.NewGridWithColumns(2, cards...),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
s.card = card
|
||||||
|
cards = append(cards, card)
|
||||||
|
stat.cards = append(stat.cards, card)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doUpdate(c *Config, status *xinStatus) error {
|
stat.upgradeProgress = widget.NewProgressBar()
|
||||||
for _, h := range c.Statuses {
|
stat.upgradeProgress.Min = 0
|
||||||
err := h.Update()
|
stat.upgradeProgress.Max = float64(len(stat.config.Statuses))
|
||||||
if err != nil {
|
stat.upgradeProgress.TextFormatter = func() string {
|
||||||
status.prependLog(err.Error())
|
return fmt.Sprintf("%.0f of %.0f hosts up-to-date",
|
||||||
|
stat.upgradeProgress.Value, stat.upgradeProgress.Max)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
bsCommitMsg := binding.BindString(&stat.repoCommitMsg)
|
||||||
|
bsCommitHash := binding.BindString(&stat.repoCommitHash)
|
||||||
|
|
||||||
|
stat.boundStrings = append(stat.boundStrings, bsCommitMsg)
|
||||||
|
stat.boundStrings = append(stat.boundStrings, bsCommitHash)
|
||||||
|
|
||||||
|
statusCard := widget.NewCard("Xin Status", "", container.NewVBox(
|
||||||
|
widget.NewLabelWithData(bsCommitMsg),
|
||||||
|
stat.upgradeProgress,
|
||||||
|
))
|
||||||
|
stat.cards = append(cards, statusCard)
|
||||||
|
return container.NewVBox(
|
||||||
|
statusCard,
|
||||||
|
container.NewGridWithColumns(3, cards...),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
status := &xinStatus{}
|
status := &xinStatus{}
|
||||||
data := &Config{}
|
|
||||||
dataPath := path.Clean(path.Join(os.Getenv("HOME"), ".xin.json"))
|
dataPath := path.Clean(path.Join(os.Getenv("HOME"), ".xin.json"))
|
||||||
err := data.Load(dataPath)
|
err := status.config.Load(dataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statusPKPath = data.PrivKeyPath
|
|
||||||
|
|
||||||
a := app.New()
|
a := app.New()
|
||||||
w := a.NewWindow("xintray")
|
w := a.NewWindow("xintray")
|
||||||
|
|
||||||
tabs := container.NewAppTabs(
|
tabs := container.NewAppTabs(
|
||||||
container.NewTabItemWithIcon("Status", theme.ComputerIcon(), buildCards(data, status)),
|
container.NewTabItemWithIcon("Status", theme.ComputerIcon(), buildCards(status)),
|
||||||
)
|
)
|
||||||
|
|
||||||
status.tabs = tabs
|
status.tabs = tabs
|
||||||
status.log = widget.NewTextGrid()
|
status.log = widget.NewTextGrid()
|
||||||
|
|
||||||
err = doUpdate(data, status)
|
err = status.updateRepoInfo()
|
||||||
|
if err != nil {
|
||||||
|
status.log.SetText(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = status.updateHostInfo()
|
||||||
|
if err != nil {
|
||||||
|
status.log.SetText(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
log.Println("updating host info")
|
err = status.updateRepoInfo()
|
||||||
err = doUpdate(data, status)
|
if err != nil {
|
||||||
|
status.log.SetText(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = status.updateHostInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status.log.SetText(err.Error())
|
status.log.SetText(err.Error())
|
||||||
}
|
}
|
||||||
for _, s := range status.boundStrings {
|
for _, s := range status.boundStrings {
|
||||||
s.Reload()
|
s.Reload()
|
||||||
}
|
}
|
||||||
time.Sleep(1 * time.Minute)
|
for _, s := range status.boundBools {
|
||||||
|
s.Reload()
|
||||||
|
}
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, s := range data.Statuses {
|
for _, s := range status.config.Statuses {
|
||||||
tabs.Append(container.NewTabItem(s.Host, s.ToTable()))
|
tabs.Append(container.NewTabItem(s.Host, s.ToTable()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,6 +373,13 @@ func main() {
|
|||||||
w.Show()
|
w.Show()
|
||||||
}))
|
}))
|
||||||
desk.SetSystemTrayMenu(m)
|
desk.SetSystemTrayMenu(m)
|
||||||
|
go func() {
|
||||||
|
if status.uptodate() {
|
||||||
|
desk.SetSystemTrayIcon(theme.CheckButtonCheckedIcon())
|
||||||
|
} else {
|
||||||
|
desk.SetSystemTrayIcon(theme.CheckButtonIcon())
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
status.log.SetText("starting...")
|
status.log.SetText("starting...")
|
||||||
|
Loading…
Reference in New Issue
Block a user