mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-09-03 20:13:43 +00:00
114 lines
3.6 KiB
Go
114 lines
3.6 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"os/exec"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/user/evote/pkg/party"
|
||
|
|
"github.com/user/evote/pkg/trace"
|
||
|
|
)
|
||
|
|
|
||
|
|
// runCockpitTmux runs the ceremony and shows one tmux pane per stakeholder, each
|
||
|
|
// rendering that party's live crypto activity as ASCII/Unicode math. The panes
|
||
|
|
// follow a shared NDJSON event file that the ceremony writes to, paced.
|
||
|
|
func runCockpitTmux() error {
|
||
|
|
if _, err := exec.LookPath("tmux"); err != nil {
|
||
|
|
return fmt.Errorf("tmux not found on PATH: %w", err)
|
||
|
|
}
|
||
|
|
if os.Getenv("TMUX") != "" {
|
||
|
|
return fmt.Errorf("already inside a tmux session — run `evote cockpit --tmux` from a plain terminal (tmux cannot nest an attach)")
|
||
|
|
}
|
||
|
|
exe, err := os.Executable()
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// Shared event file the panes tail and the ceremony appends to.
|
||
|
|
f, err := os.CreateTemp("", "evote-cockpit-*.ndjson")
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
eventPath := f.Name()
|
||
|
|
defer os.Remove(eventPath)
|
||
|
|
|
||
|
|
// One pane per infrastructure party, plus one aggregate pane for all voters.
|
||
|
|
panes := []string{
|
||
|
|
party.NameSetup,
|
||
|
|
party.CCName(0), party.CCName(1), party.CCName(2), party.CCName(3),
|
||
|
|
party.NameEB, party.NameServer, party.NameVerifier,
|
||
|
|
"voters",
|
||
|
|
}
|
||
|
|
|
||
|
|
session := "evote-cockpit"
|
||
|
|
_ = exec.Command("tmux", "kill-session", "-t", session).Run() // best-effort clean slate
|
||
|
|
|
||
|
|
paneCmd := func(role string) string {
|
||
|
|
return fmt.Sprintf("%s panelview --role=%s --file=%s", exe, role, eventPath)
|
||
|
|
}
|
||
|
|
|
||
|
|
// First pane creates the (detached) session.
|
||
|
|
if out, err := exec.Command("tmux", "new-session", "-d", "-s", session, paneCmd(panes[0])).CombinedOutput(); err != nil {
|
||
|
|
return fmt.Errorf("tmux new-session: %v: %s", err, out)
|
||
|
|
}
|
||
|
|
for _, role := range panes[1:] {
|
||
|
|
if out, err := exec.Command("tmux", "split-window", "-t", session, paneCmd(role)).CombinedOutput(); err != nil {
|
||
|
|
exec.Command("tmux", "kill-session", "-t", session).Run()
|
||
|
|
return fmt.Errorf("tmux split-window: %v: %s", err, out)
|
||
|
|
}
|
||
|
|
// Re-tile after each split so panes stay balanced.
|
||
|
|
exec.Command("tmux", "select-layout", "-t", session, "tiled").Run()
|
||
|
|
}
|
||
|
|
exec.Command("tmux", "select-layout", "-t", session, "tiled").Run()
|
||
|
|
exec.Command("tmux", "set-option", "-t", session, "mouse", "on").Run()
|
||
|
|
|
||
|
|
// Run the ceremony in the background, writing paced NDJSON to the file.
|
||
|
|
go writeCeremonyEvents(f)
|
||
|
|
|
||
|
|
fmt.Printf("Launching tmux cockpit '%s' — %d panes. Detach with Ctrl-b d; it closes when you exit.\n", session, len(panes))
|
||
|
|
time.Sleep(400 * time.Millisecond) // let panes open the file first
|
||
|
|
|
||
|
|
// Attach in the foreground; blocks until the user detaches or exits.
|
||
|
|
attach := exec.Command("tmux", "attach", "-t", session)
|
||
|
|
attach.Stdin, attach.Stdout, attach.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||
|
|
err = attach.Run()
|
||
|
|
|
||
|
|
exec.Command("tmux", "kill-session", "-t", session).Run()
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("tmux attach failed (run this from an interactive terminal): %w", err)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// writeCeremonyEvents subscribes a paced file sink, runs the full ceremony, and
|
||
|
|
// writes a final done marker.
|
||
|
|
func writeCeremonyEvents(f *os.File) {
|
||
|
|
delay := time.Duration(cockpitDelayMs) * time.Millisecond
|
||
|
|
sink := &fileSink{f: f, delay: delay, enc: json.NewEncoder(f)}
|
||
|
|
unsub := trace.Subscribe(sink)
|
||
|
|
defer unsub()
|
||
|
|
|
||
|
|
_ = runCockpitCeremony()
|
||
|
|
|
||
|
|
f.WriteString(`{"done":true}` + "\n")
|
||
|
|
f.Sync()
|
||
|
|
}
|
||
|
|
|
||
|
|
// fileSink appends each event as one JSON line, pacing between events so the
|
||
|
|
// panes render at a watchable rate.
|
||
|
|
type fileSink struct {
|
||
|
|
f *os.File
|
||
|
|
delay time.Duration
|
||
|
|
enc *json.Encoder
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *fileSink) Handle(e trace.Event) {
|
||
|
|
_ = s.enc.Encode(e) // Encode writes the trailing newline
|
||
|
|
s.f.Sync()
|
||
|
|
if s.delay > 0 {
|
||
|
|
time.Sleep(s.delay)
|
||
|
|
}
|
||
|
|
}
|