diff --git a/cmd/d2plugin-dagre/main.go b/cmd/d2plugin-dagre/main.go index 20cb9af5a..a2350dc4e 100644 --- a/cmd/d2plugin-dagre/main.go +++ b/cmd/d2plugin-dagre/main.go @@ -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() { diff --git a/d2plugin/plugin.go b/d2plugin/plugin.go index 24d9ad379..56798c88f 100644 --- a/d2plugin/plugin.go +++ b/d2plugin/plugin.go @@ -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. diff --git a/d2plugin/serve.go b/d2plugin/serve.go index 2db42d215..e7ca429aa 100644 --- a/d2plugin/serve.go +++ b/d2plugin/serve.go @@ -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 diff --git a/fmt.go b/fmt.go index 49fd5677b..3f711414c 100644 --- a/fmt.go +++ b/fmt.go @@ -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) { diff --git a/go.mod b/go.mod index dc61ded3f..dc25f4adf 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 9ca23e3de..5ca589004 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/help.go b/help.go index 6f2f93637..bd6b455d0 100644 --- a/help.go +++ b/help.go @@ -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) { diff --git a/lib/imgbundler/imgbundler.go b/lib/imgbundler/imgbundler.go index 0935f8637..7d52a69e0 100644 --- a/lib/imgbundler/imgbundler.go +++ b/lib/imgbundler/imgbundler.go @@ -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 diff --git a/lib/imgbundler/imgbundler_test.go b/lib/imgbundler/imgbundler_test.go index ad5e4fea9..0c06ab891 100644 --- a/lib/imgbundler/imgbundler_test.go +++ b/lib/imgbundler/imgbundler_test.go @@ -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 diff --git a/lib/png/png.go b/lib/png/png.go index 0199c3c91..80d4a51e1 100644 --- a/lib/png/png.go +++ b/lib/png/png.go @@ -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 { diff --git a/lib/xhttp/err.go b/lib/xhttp/err.go deleted file mode 100644 index e983f194c..000000000 --- a/lib/xhttp/err.go +++ /dev/null @@ -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) -} diff --git a/lib/xhttp/log.go b/lib/xhttp/log.go deleted file mode 100644 index 0587cbef9..000000000 --- a/lib/xhttp/log.go +++ /dev/null @@ -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) - }) -} diff --git a/lib/xhttp/serve.go b/lib/xhttp/serve.go deleted file mode 100644 index e7c7c1a24..000000000 --- a/lib/xhttp/serve.go +++ /dev/null @@ -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) - } -} diff --git a/lib/xmain/flag_helpers.go b/lib/xmain/flag_helpers.go deleted file mode 100644 index 4d17066dc..000000000 --- a/lib/xmain/flag_helpers.go +++ /dev/null @@ -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:] -} diff --git a/lib/xmain/opts.go b/lib/xmain/opts.go deleted file mode 100644 index 22a96b0ab..000000000 --- a/lib/xmain/opts.go +++ /dev/null @@ -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" -} diff --git a/lib/xmain/xmain.go b/lib/xmain/xmain.go deleted file mode 100644 index b0a1aeb3a..000000000 --- a/lib/xmain/xmain.go +++ /dev/null @@ -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))) -} diff --git a/main.go b/main.go index 29ace7b16..58745abf1 100644 --- a/main.go +++ b/main.go @@ -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))) +} diff --git a/watch.go b/watch.go index ca928c026..60b8e90c0 100644 --- a/watch.go +++ b/watch.go @@ -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".