1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
// alloy-viz renders Alloy instances in a browser, without the Alloy GUI.
//
// Point it at a .als file; it solves the selected command and shows the instance
// as a graph, re-solving whenever you save. Keep editing in your own editor --
// the browser is a viewer.
//
// One model per process: to watch a second file, run a second alloy-viz on
// another port.
package main
import (
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
)
func main() {
log.SetFlags(log.Ltime)
var (
addr = flag.String("addr", "127.0.0.1:8791", "address for the web UI")
noOpen = flag.Bool("no-run", false, "do not solve on startup; wait for a command to be chosen")
commandIdx = flag.Int("command", 0, "index of the command to run initially")
)
flag.Usage = usage
flag.Parse()
if flag.NArg() != 1 {
usage()
os.Exit(2)
}
path, err := filepath.Abs(flag.Arg(0))
if err != nil {
fatal(err)
}
if _, err := os.Stat(path); err != nil {
fatal(err)
}
if !strings.HasSuffix(path, ".als") {
log.Printf("warning: %s does not look like an Alloy model", path)
}
srv := newServer(path)
srv.selected = *commandIdx
defer srv.Shutdown()
// Ctrl-C must kill the JVM too, or it lingers holding a SAT solver.
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigs
log.Print("shutting down")
srv.Shutdown()
os.Exit(0)
}()
// Watch before the first solve, so an edit during a slow startup is not lost.
go func() {
if err := watchFile(srv.path, func() {
log.Printf("%s changed, re-running", filepath.Base(srv.path))
srv.Invalidate()
srv.Reload(true)
}); err != nil {
log.Printf("watch: %v", err)
}
}()
go srv.Reload(!*noOpen)
ln, err := net.Listen("tcp", *addr)
if err != nil {
fatal(err)
}
log.Printf("alloy-viz on http://%s (%s)", ln.Addr(), srv.path)
httpSrv := &http.Server{
Handler: srv.Handler(),
// No write timeout: SSE streams stay open for the length of a solve,
// which can exceed any sensible bound.
ReadHeaderTimeout: 10 * time.Second,
}
if err := httpSrv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
fatal(err)
}
}
func usage() {
fmt.Fprintf(os.Stderr, `alloy-viz — view Alloy instances in a browser
usage: alloy-viz [flags] <model.als>
Solves a command from the model and renders the instance as a graph, re-solving
on every save. A sibling <model>.thm theme file is applied if present.
flags:
`)
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, `
environment:
%s path to the alloy-viz-solver binary (the nix wrapper sets this)
examples:
alloy-viz model.als
alloy-viz -command 3 filesystem.als
alloy-viz -no-run model.als # pick the command in the browser first
`, solverPathEnv)
}
func fatal(err error) {
fmt.Fprintf(os.Stderr, "alloy-viz: %v\n", err)
os.Exit(1)
}
|