xmain: rm -rf

This commit is contained in:
Anmol Sethi 2022-12-01 11:19:39 -08:00
parent 820226c72b
commit 07e316fac5
No known key found for this signature in database
GPG key ID: 25BC68888A99A8BA
18 changed files with 36 additions and 761 deletions

View file

@ -3,8 +3,9 @@
package main
import (
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/lib/xmain"
)
func main() {

View file

@ -9,8 +9,9 @@ import (
"context"
"os/exec"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/util-go/xexec"
"oss.terrastruct.com/d2/d2graph"
)
// plugins contains the bundled d2 plugins.

View file

@ -7,8 +7,9 @@ import (
"fmt"
"io"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/xmain"
)
// Serve returns a xmain.RunFunc that will invoke the plugin p as necessary to service the

3
fmt.go
View file

@ -6,9 +6,10 @@ import (
"oss.terrastruct.com/xdefer"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/lib/xmain"
)
func fmtCmd(ctx context.Context, ms *xmain.State) (err error) {

7
go.mod generated
View file

@ -17,17 +17,13 @@ require (
go.uber.org/multierr v1.8.0
golang.org/x/image v0.1.0
golang.org/x/net v0.2.0
golang.org/x/text v0.4.0
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
gonum.org/v1/plot v0.12.0
nhooyr.io/websocket v1.8.7
oss.terrastruct.com/cmdlog v0.0.0-20221201100934-012c01b3431c
oss.terrastruct.com/diff v1.0.2-0.20221116222035-8bf4dd3ab541
oss.terrastruct.com/util-go v0.0.0-20221201190418-569dcbf6dc3f
oss.terrastruct.com/xcontext v0.0.0-20221018000442-50fdafb12f4f
oss.terrastruct.com/util-go v0.0.0-20221201191904-5edc89ce397b
oss.terrastruct.com/xdefer v0.0.0-20221017222355-6f3b6e4d1557
oss.terrastruct.com/xjson v0.0.0-20221018000420-4986731c4c4a
oss.terrastruct.com/xos v0.0.0-20221130233107-5fb84d57c9e3
oss.terrastruct.com/xrand v0.0.0-20221020211818-4ac08e618333
rogchap.com/v8go v0.7.1-0.20221102201510-1f00b5007d95
)
@ -59,6 +55,7 @@ require (
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/term v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect

12
go.sum generated
View file

@ -798,22 +798,14 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
oss.terrastruct.com/cmdlog v0.0.0-20221201100934-012c01b3431c h1:C1DDLzj2NrVi1YJpbZluEYZ2MkpJGppZYfSQ+F87TT0=
oss.terrastruct.com/cmdlog v0.0.0-20221201100934-012c01b3431c/go.mod h1:C8u/lYTvQWc1xC7rHpgFfpScfQC4NMeGGMmlKVZZUXM=
oss.terrastruct.com/diff v1.0.2-0.20221116222035-8bf4dd3ab541 h1:I9B1O1IJ6spivIQxbFRZmbhAwVeLwrcQRR1JbYUOvrI=
oss.terrastruct.com/diff v1.0.2-0.20221116222035-8bf4dd3ab541/go.mod h1:ags2QDy/T6jr69hT6bpmAmhr2H98n9o8Atf3QlUJPiU=
oss.terrastruct.com/util-go v0.0.0-20221201185848-8cc30ca56bbe h1:1CTXmBqea1vbVhYsyZ3NiCYUFZgURIQF/ItjrdlhwlE=
oss.terrastruct.com/util-go v0.0.0-20221201185848-8cc30ca56bbe/go.mod h1:bL3CBn27CtTm++1iqRh2p6f8AIWeTdtlTN199Kg9JYM=
oss.terrastruct.com/util-go v0.0.0-20221201190418-569dcbf6dc3f h1:ADVDj5TeMMxXMEKAxC9RKLCQq4gaW6GKynAeojB9VQQ=
oss.terrastruct.com/util-go v0.0.0-20221201190418-569dcbf6dc3f/go.mod h1:bL3CBn27CtTm++1iqRh2p6f8AIWeTdtlTN199Kg9JYM=
oss.terrastruct.com/xcontext v0.0.0-20221018000442-50fdafb12f4f h1:7voRCwKM7TZkTo9u7hj+uV/zXoVB8czWrTq6MVIh3dg=
oss.terrastruct.com/xcontext v0.0.0-20221018000442-50fdafb12f4f/go.mod h1:Y0coTLsWwX0q3a+/Ndq797t+vWyxm42T49Ik3bzaDKY=
oss.terrastruct.com/util-go v0.0.0-20221201191904-5edc89ce397b h1:o8+5KfZpQyaw7uKcPIdc9HOqVjVDEdsPZpdRV1k0rmc=
oss.terrastruct.com/util-go v0.0.0-20221201191904-5edc89ce397b/go.mod h1:Fwy72FDIOOM4K8F96ScXkxHHppR1CPfUyo9+x9c1PBU=
oss.terrastruct.com/xdefer v0.0.0-20221017222355-6f3b6e4d1557 h1:rPbhJbN1q7B4tnppSPoAMwq0t6Pk5SrQDQ5S6uoNNHg=
oss.terrastruct.com/xdefer v0.0.0-20221017222355-6f3b6e4d1557/go.mod h1:plvfydF5METAlsbpeuSz44jckaOwrCWX3M0kTLoCA4I=
oss.terrastruct.com/xjson v0.0.0-20221018000420-4986731c4c4a h1:AAcupsjBwpbcyLASX0ppDlxbfHWb5Neq5gWdGpLfaSA=
oss.terrastruct.com/xjson v0.0.0-20221018000420-4986731c4c4a/go.mod h1:XJ71qiTzk/dbTWuYbuLJuRpBdKFN06Sk5FdFpq2TNmE=
oss.terrastruct.com/xos v0.0.0-20221130233107-5fb84d57c9e3 h1:bchGZ5WryJNqr/yZ00rTcgZh1AComRFwKKBWOIxzJZE=
oss.terrastruct.com/xos v0.0.0-20221130233107-5fb84d57c9e3/go.mod h1:lUSNCN0HA3pWHOMXT6gRNJtjg1U5t0TEEwAzPyV6enA=
oss.terrastruct.com/xrand v0.0.0-20221020211818-4ac08e618333 h1:7EdxwXM75Id1VIN71QbE8bLzZRMs0qD7olnDw5gbI7w=
oss.terrastruct.com/xrand v0.0.0-20221020211818-4ac08e618333/go.mod h1:O7TAoBmlQhoi46RdgVikDcoLRb/vLflhkXCAd+nO4SM=
rogchap.com/v8go v0.7.1-0.20221102201510-1f00b5007d95 h1:r89YHVIWeQj/A3Nu6462eqARUECJlJkLRk36pfML1xA=

View file

@ -9,8 +9,9 @@ import (
"path/filepath"
"strings"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/lib/xmain"
)
func help(ms *xmain.State) {

View file

@ -19,7 +19,7 @@ import (
"golang.org/x/xerrors"
"oss.terrastruct.com/xdefer"
"oss.terrastruct.com/d2/lib/xmain"
"oss.terrastruct.com/util-go/xmain"
)
const maxImageSize int64 = 1 << 25 // 33_554_432

View file

@ -12,10 +12,10 @@ import (
"strings"
"testing"
"oss.terrastruct.com/cmdlog"
"oss.terrastruct.com/xos"
"oss.terrastruct.com/util-go/cmdlog"
"oss.terrastruct.com/util-go/xos"
"oss.terrastruct.com/d2/lib/xmain"
"oss.terrastruct.com/util-go/xmain"
)
//go:embed test_png.png

View file

@ -9,7 +9,7 @@ import (
"github.com/playwright-community/playwright-go"
"oss.terrastruct.com/d2/lib/xmain"
"oss.terrastruct.com/util-go/xmain"
)
type Playwright struct {

View file

@ -1,165 +0,0 @@
package xhttp
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"oss.terrastruct.com/cmdlog"
)
// Error represents an HTTP error.
// It's exported only for comparison in tests.
type Error struct {
Code int
Resp interface{}
Err error
}
var _ interface {
Is(error) bool
Unwrap() error
} = Error{}
// Errorf creates a new error with code, resp, msg and v.
//
// When returned from an xhttp.HandlerFunc, it will be correctly logged
// and written to the connection. See xhttp.WrapHandlerFunc
func Errorf(code int, resp interface{}, msg string, v ...interface{}) error {
return errorWrap(code, resp, fmt.Errorf(msg, v...))
}
// ErrorWrap wraps err with the code and resp for xhttp.HandlerFunc.
//
// When returned from an xhttp.HandlerFunc, it will be correctly logged
// and written to the connection. See xhttp.WrapHandlerFunc
func ErrorWrap(code int, resp interface{}, err error) error {
return errorWrap(code, resp, err)
}
func errorWrap(code int, resp interface{}, err error) error {
if resp == nil {
resp = http.StatusText(code)
}
return Error{code, resp, err}
}
func (e Error) Unwrap() error {
return e.Err
}
func (e Error) Is(err error) bool {
e2, ok := err.(Error)
if !ok {
return false
}
return e.Code == e2.Code && e.Resp == e2.Resp && errors.Is(e.Err, e2.Err)
}
func (e Error) Error() string {
return fmt.Sprintf("http error with code %v and resp %#v: %v", e.Code, e.Resp, e.Err)
}
// HandlerFunc is like http.HandlerFunc but returns an error.
// See Errorf and ErrorWrap.
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
type HandlerFuncAdapter struct {
Log *cmdlog.Logger
Func HandlerFunc
}
// ServeHTTP adapts xhttp.HandlerFunc into http.Handler for usage with standard
// HTTP routers like chi.
//
// It logs and writes any error from xhttp.HandlerFunc to the connection.
//
// If err was created with xhttp.Errorf or wrapped with xhttp.WrapError, then the error
// will be logged at the correct level for the status code and xhttp.JSON will be called
// with the code and resp.
//
// 400s are logged as warns and 500s as errors.
//
// If the error was not created with the xhttp helpers then a 500 will be written.
//
// If resp is nil, then resp is set to http.StatusText(code)
//
// If the code is not a 400 or a 500, then an error about about the unexpected error code
// will be logged and a 500 will be written. The original error will also be logged.
func (a HandlerFuncAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := a.Func(w, r)
if err != nil {
handleError(a.Log, w, err)
}
})
h.ServeHTTP(w, r)
}
func handleError(clog *cmdlog.Logger, w http.ResponseWriter, err error) {
var herr Error
ok := errors.As(err, &herr)
if !ok {
herr = ErrorWrap(http.StatusInternalServerError, nil, err).(Error)
}
var logger *log.Logger
switch {
case 400 <= herr.Code && herr.Code < 500:
logger = clog.Warn
case 500 <= herr.Code && herr.Code < 600:
logger = clog.Error
default:
logger = clog.Error
clog.Error.Printf("unexpected non error http status code %d with resp: %#v", herr.Code, herr.Resp)
herr.Code = http.StatusInternalServerError
herr.Resp = nil
}
if herr.Resp == nil {
herr.Resp = http.StatusText(herr.Code)
}
logger.Printf("error handling http request: %v", err)
ww, ok := w.(writtenResponseWriter)
if !ok {
clog.Warn.Printf("response writer does not implement Written, double write logs possible: %#v", w)
} else if ww.Written() {
// Avoid double writes if an error occurred while the response was
// being written.
return
}
JSON(clog, w, herr.Code, map[string]interface{}{
"error": herr.Resp,
})
}
type writtenResponseWriter interface {
Written() bool
}
func JSON(clog *cmdlog.Logger, w http.ResponseWriter, code int, v interface{}) {
if v == nil {
v = map[string]interface{}{
"status": http.StatusText(code),
}
}
b, err := json.Marshal(v)
if err != nil {
clog.Error.Printf("json marshal error: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_, _ = w.Write(b)
}

View file

@ -1,125 +0,0 @@
package xhttp
import (
"bufio"
"errors"
"fmt"
"log"
"net"
"net/http"
"runtime/debug"
"time"
"golang.org/x/text/message"
"oss.terrastruct.com/cmdlog"
)
type ResponseWriter interface {
http.ResponseWriter
http.Hijacker
http.Flusher
writtenResponseWriter
}
var _ ResponseWriter = &responseWriter{}
type responseWriter struct {
rw http.ResponseWriter
written bool
status int
length int
}
func (rw *responseWriter) Header() http.Header {
return rw.rw.Header()
}
func (rw *responseWriter) WriteHeader(statusCode int) {
if !rw.written {
rw.written = true
rw.status = statusCode
}
rw.rw.WriteHeader(statusCode)
}
func (rw *responseWriter) Write(p []byte) (int, error) {
if !rw.written && len(p) > 0 {
rw.written = true
if rw.status == 0 {
rw.status = http.StatusOK
}
}
rw.length += len(p)
return rw.rw.Write(p)
}
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hj, ok := rw.rw.(http.Hijacker)
if !ok {
return nil, nil, fmt.Errorf("underlying response writer does not implement http.Hijacker: %T", rw.rw)
}
return hj.Hijack()
}
func (rw *responseWriter) Flush() {
f, ok := rw.rw.(http.Flusher)
if !ok {
return
}
f.Flush()
}
func (rw *responseWriter) Written() bool {
return rw.written
}
func Log(clog *cmdlog.Logger, next http.Handler) http.Handler {
englishPrinter := message.NewPrinter(message.MatchLanguage("en"))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
rec := recover()
if rec != nil {
clog.Error.Printf("caught panic: %#v\n%s", rec, debug.Stack())
JSON(clog, w, http.StatusInternalServerError, map[string]interface{}{
"error": http.StatusText(http.StatusInternalServerError),
})
}
}()
rw := &responseWriter{
rw: w,
}
start := time.Now()
next.ServeHTTP(rw, r)
dur := time.Since(start)
if !rw.Written() {
_, err := rw.Write(nil)
if errors.Is(err, http.ErrHijacked) {
clog.Success.Printf("%s %s %v: hijacked", r.Method, r.URL, dur)
return
}
clog.Warn.Printf("%s %s %v: no response written", r.Method, r.URL, dur)
return
}
var statusLogger *log.Logger
switch {
case 100 <= rw.status && rw.status <= 299:
statusLogger = clog.Success
case 300 <= rw.status && rw.status <= 399:
statusLogger = clog.Info
case 400 <= rw.status && rw.status <= 499:
statusLogger = clog.Warn
case 500 <= rw.status && rw.status <= 599:
statusLogger = clog.Error
}
lengthStr := englishPrinter.Sprint(rw.length)
// TODO: make work with watch.go on hijack, not after
statusLogger.Printf("%s %s %d %sB %v", r.Method, r.URL, rw.status, lengthStr, dur)
})
}

View file

@ -1,44 +0,0 @@
// Package xhttp implements http helpers.
package xhttp
import (
"context"
"log"
"net"
"net/http"
"time"
"oss.terrastruct.com/xcontext"
)
func NewServer(log *log.Logger, h http.Handler) *http.Server {
return &http.Server{
MaxHeaderBytes: 1 << 18, // 262,144B
ReadTimeout: time.Minute,
WriteTimeout: time.Minute,
IdleTimeout: time.Hour,
ErrorLog: log,
Handler: http.MaxBytesHandler(h, 1<<20), // 1,048,576B
}
}
func Serve(ctx context.Context, shutdownTimeout time.Duration, s *http.Server, l net.Listener) error {
s.BaseContext = func(net.Listener) context.Context {
return ctx
}
done := make(chan error, 1)
go func() {
done <- s.Serve(l)
}()
select {
case err := <-done:
return err
case <-ctx.Done():
ctx = xcontext.WithoutCancel(ctx)
ctx, cancel := context.WithTimeout(ctx, shutdownTimeout)
defer cancel()
return s.Shutdown(ctx)
}
}

View file

@ -1,45 +0,0 @@
// flag_helpers.go are private functions from pflag/flag.go
package xmain
import "strings"
func wrap(i, w int, s string) string {
if w == 0 {
return strings.Replace(s, "\n", "\n"+strings.Repeat(" ", i), -1)
}
wrap := w - i
var r, l string
if wrap < 24 {
i = 16
wrap = w - i
r += "\n" + strings.Repeat(" ", i)
}
if wrap < 24 {
return strings.Replace(s, "\n", r, -1)
}
slop := 5
wrap = wrap - slop
l, s = wrapN(wrap, slop, s)
r = r + strings.Replace(l, "\n", "\n"+strings.Repeat(" ", i), -1)
for s != "" {
var t string
t, s = wrapN(wrap, slop, s)
r = r + "\n" + strings.Repeat(" ", i) + strings.Replace(t, "\n", "\n"+strings.Repeat(" ", i), -1)
}
return r
}
func wrapN(i, slop int, s string) (string, string) {
if i+slop > len(s) {
return s, ""
}
w := strings.LastIndexAny(s[:i], " \t\n")
if w <= 0 {
return s, ""
}
nlPos := strings.LastIndex(s[:i], "\n")
if nlPos > 0 && nlPos < w {
return s[:nlPos], s[nlPos+1:]
}
return s[:w], s[w+1:]
}

View file

@ -1,173 +0,0 @@
package xmain
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"github.com/spf13/pflag"
"oss.terrastruct.com/cmdlog"
"oss.terrastruct.com/xos"
)
type Opts struct {
Args []string
Flags *pflag.FlagSet
env *xos.Env
log *cmdlog.Logger
flagEnv map[string]string
}
func NewOpts(env *xos.Env, log *cmdlog.Logger, args []string) *Opts {
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
flags.SortFlags = false
flags.Usage = func() {}
flags.SetOutput(io.Discard)
return &Opts{
Args: args,
Flags: flags,
env: env,
log: log,
flagEnv: make(map[string]string),
}
}
// Mostly copy pasted pasted from pflag.FlagUsagesWrapped
// with modifications for env var
func (o *Opts) Defaults() string {
buf := new(bytes.Buffer)
var lines []string
maxlen := 0
maxEnvLen := 0
o.Flags.VisitAll(func(flag *pflag.Flag) {
if flag.Hidden {
return
}
line := ""
if flag.Shorthand != "" && flag.ShorthandDeprecated == "" {
line = fmt.Sprintf(" -%s, --%s", flag.Shorthand, flag.Name)
} else {
line = fmt.Sprintf(" --%s", flag.Name)
}
varname, usage := pflag.UnquoteUsage(flag)
if varname != "" {
line += " " + varname
}
if flag.NoOptDefVal != "" {
switch flag.Value.Type() {
case "string":
line += fmt.Sprintf("[=\"%s\"]", flag.NoOptDefVal)
case "bool":
if flag.NoOptDefVal != "true" {
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
}
case "count":
if flag.NoOptDefVal != "+1" {
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
}
default:
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
}
}
line += "\x00"
if len(line) > maxlen {
maxlen = len(line)
}
if e, ok := o.flagEnv[flag.Name]; ok {
line += fmt.Sprintf("$%s", e)
}
line += "\x01"
if len(line) > maxEnvLen {
maxEnvLen = len(line)
}
line += usage
if flag.Value.Type() == "string" {
line += fmt.Sprintf(" (default %q)", flag.DefValue)
} else {
line += fmt.Sprintf(" (default %s)", flag.DefValue)
}
if len(flag.Deprecated) != 0 {
line += fmt.Sprintf(" (DEPRECATED: %s)", flag.Deprecated)
}
lines = append(lines, line)
})
for _, line := range lines {
sidx1 := strings.Index(line, "\x00")
sidx2 := strings.Index(line, "\x01")
spacing1 := strings.Repeat(" ", maxlen-sidx1)
spacing2 := strings.Repeat(" ", (maxEnvLen-maxlen)-sidx2+sidx1)
fmt.Fprintln(buf, line[:sidx1], spacing1, line[sidx1+1:sidx2], spacing2, wrap(maxEnvLen+3, 0, line[sidx2+1:]))
}
return buf.String()
}
func (o *Opts) getEnv(flag, k string) string {
if k != "" {
o.flagEnv[flag] = k
return o.env.Getenv(k)
}
return ""
}
func (o *Opts) Int64(envKey, flag, shortFlag string, defaultVal int64, usage string) (*int64, error) {
if env := o.getEnv(flag, envKey); env != "" {
envVal, err := strconv.ParseInt(env, 10, 64)
if err != nil {
return nil, UsageErrorf(`invalid environment variable %s. Expected int64. Found "%v".`, envKey, envVal)
}
defaultVal = envVal
}
return o.Flags.Int64P(flag, shortFlag, defaultVal, usage), nil
}
func (o *Opts) String(envKey, flag, shortFlag string, defaultVal, usage string) *string {
if env := o.getEnv(flag, envKey); env != "" {
defaultVal = env
}
return o.Flags.StringP(flag, shortFlag, defaultVal, usage)
}
func (o *Opts) Bool(envKey, flag, shortFlag string, defaultVal bool, usage string) (*bool, error) {
if env := o.getEnv(flag, envKey); env != "" {
if !boolyEnv(env) {
return nil, UsageErrorf(`invalid environment variable %s. Expected bool. Found "%s".`, envKey, env)
}
if truthyEnv(env) {
defaultVal = true
} else {
defaultVal = false
}
}
return o.Flags.BoolP(flag, shortFlag, defaultVal, usage), nil
}
func boolyEnv(s string) bool {
return falseyEnv(s) || truthyEnv(s)
}
func falseyEnv(s string) bool {
return s == "0" || s == "false"
}
func truthyEnv(s string) bool {
return s == "1" || s == "true"
}

View file

@ -1,180 +0,0 @@
// Package xmain provides a standard stub for the main of a command handling logging,
// flags, signals and shutdown.
package xmain
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/signal"
"syscall"
"time"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"oss.terrastruct.com/xos"
"oss.terrastruct.com/cmdlog"
ctxlog "oss.terrastruct.com/d2/lib/log"
)
type RunFunc func(context.Context, *State) error
func Main(run RunFunc) {
name := ""
args := []string(nil)
if len(os.Args) > 0 {
name = os.Args[0]
args = os.Args[1:]
}
ms := &State{
Name: name,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Env: xos.NewEnv(os.Environ()),
}
ms.Log = cmdlog.New(ms.Env, os.Stderr)
ms.Opts = NewOpts(ms.Env, ms.Log, args)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
err := ms.Main(context.Background(), sigs, run)
if err != nil {
code := 1
msg := ""
usage := false
var eerr ExitError
var uerr UsageError
if errors.As(err, &eerr) {
code = eerr.Code
msg = eerr.Message
} else if errors.As(err, &uerr) {
msg = err.Error()
usage = true
} else {
msg = err.Error()
}
if msg != "" {
ms.Log.Error.Print(msg)
if usage {
ms.Log.Error.Print("Run with --help to see usage.")
}
}
os.Exit(code)
}
}
type State struct {
Name string
Stdin io.Reader
Stdout io.WriteCloser
Stderr io.WriteCloser
Log *cmdlog.Logger
Env *xos.Env
Opts *Opts
}
func (ms *State) Main(ctx context.Context, sigs <-chan os.Signal, run func(context.Context, *State) error) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
done := make(chan error, 1)
go func() {
defer close(done)
done <- run(ctx, ms)
}()
select {
case err := <-done:
return err
case sig := <-sigs:
ms.Log.Warn.Printf("received signal %v: shutting down...", sig)
cancel()
select {
case err := <-done:
if err != nil && !errors.Is(err, context.Canceled) {
return fmt.Errorf("failed to shutdown: %w", err)
}
if sig == syscall.SIGTERM {
// We successfully shutdown.
return nil
}
return ExitError{Code: 1}
case <-time.After(time.Minute):
return ExitError{
Code: 1,
Message: "took longer than 1 minute to shutdown: exiting forcefully",
}
}
}
}
type ExitError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func ExitErrorf(code int, msg string, v ...interface{}) ExitError {
return ExitError{
Code: code,
Message: fmt.Sprintf(msg, v...),
}
}
func (ee ExitError) Error() string {
s := fmt.Sprintf("exiting with code %d", ee.Code)
if ee.Message != "" {
s += ": " + ee.Message
}
return s
}
type UsageError struct {
Message string `json:"message"`
}
func UsageErrorf(msg string, v ...interface{}) UsageError {
return UsageError{
Message: fmt.Sprintf(msg, v...),
}
}
func (ue UsageError) Error() string {
return fmt.Sprintf("bad usage: %s", ue.Message)
}
func (ms *State) ReadPath(fp string) ([]byte, error) {
if fp == "-" {
return io.ReadAll(ms.Stdin)
}
return os.ReadFile(fp)
}
func (ms *State) WritePath(fp string, p []byte) error {
if fp == "-" {
_, err := ms.Stdout.Write(p)
if err != nil {
return err
}
return ms.Stdout.Close()
}
return os.WriteFile(fp, p, 0644)
}
// TODO: remove after removing slog
func DiscardSlog(ctx context.Context) context.Context {
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
}

15
main.go
View file

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
@ -14,6 +15,8 @@ import (
"github.com/spf13/pflag"
"go.uber.org/multierr"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2plugin"
@ -21,10 +24,13 @@ import (
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/imgbundler"
ctxlog "oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/d2/lib/version"
"oss.terrastruct.com/d2/lib/xmain"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
)
func main() {
@ -33,7 +39,7 @@ func main() {
func run(ctx context.Context, ms *xmain.State) (err error) {
// :(
ctx = xmain.DiscardSlog(ctx)
ctx = DiscardSlog(ctx)
// These should be kept up-to-date with the d2 man page
watchFlag, err := ms.Opts.Bool("D2_WATCH", "watch", "w", false, "watch for changes to input and live reload. Use $HOST and $PORT to specify the listening address.\n(default localhost:0, which is will open on a randomly available local port).")
@ -257,3 +263,8 @@ func renameExt(fp string, newExt string) string {
return strings.TrimSuffix(fp, ext) + newExt
}
}
// TODO: remove after removing slog
func DiscardSlog(ctx context.Context) context.Context {
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
}

View file

@ -21,10 +21,12 @@ import (
"oss.terrastruct.com/util-go/xbrowser"
"oss.terrastruct.com/util-go/xhttp"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/xhttp"
"oss.terrastruct.com/d2/lib/xmain"
)
// Enabled with the build tag "dev".