mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
332 lines
9.2 KiB
Go
332 lines
9.2 KiB
Go
// The MIT License
|
|
//
|
|
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
|
|
//
|
|
// Copyright (c) 2020 Uber Technologies, Inc.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
package tdbg
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/gogo/protobuf/proto"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/urfave/cli/v2"
|
|
"go.temporal.io/api/workflowservice/v1"
|
|
|
|
"go.temporal.io/server/common/codec"
|
|
"go.temporal.io/server/common/collection"
|
|
"go.temporal.io/server/common/namespace"
|
|
)
|
|
|
|
func prettyPrintJSONObject(o interface{}) {
|
|
var b []byte
|
|
var err error
|
|
if pb, ok := o.(proto.Message); ok {
|
|
encoder := codec.NewJSONPBIndentEncoder(" ")
|
|
b, err = encoder.Encode(pb)
|
|
} else {
|
|
b, err = json.MarshalIndent(o, "", " ")
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Printf("Error when try to print pretty: %v\n", err)
|
|
fmt.Println(o)
|
|
}
|
|
_, _ = os.Stdout.Write(b)
|
|
fmt.Println()
|
|
}
|
|
|
|
func getRequiredOption(c *cli.Context, optionName string) (string, error) {
|
|
value := c.String(optionName)
|
|
if len(value) == 0 {
|
|
return "", fmt.Errorf("option is required: %s", optionName)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func parseTime(timeStr string, defaultValue time.Time, now time.Time) (time.Time, error) {
|
|
if len(timeStr) == 0 {
|
|
return defaultValue, nil
|
|
}
|
|
|
|
// try to parse
|
|
parsedTime, err := time.Parse(defaultDateTimeFormat, timeStr)
|
|
if err == nil {
|
|
return parsedTime, nil
|
|
}
|
|
|
|
// treat as raw unix time
|
|
resultValue, err := strconv.ParseInt(timeStr, 10, 64)
|
|
if err == nil {
|
|
return time.Unix(0, resultValue).UTC(), nil
|
|
}
|
|
|
|
// treat as time range format
|
|
parsedTime, err = parseTimeRange(timeStr, now)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("cannot parse time '%s', use UTC format '2006-01-02T15:04:05', "+
|
|
"time range or raw UnixNano directly. See help for more details: %s", timeStr, err)
|
|
}
|
|
return parsedTime, nil
|
|
}
|
|
|
|
// parseTimeRange parses a given time duration string (in format X<time-duration>) and
|
|
// returns parsed timestamp given that duration in the past from current time.
|
|
// All valid values must contain a number followed by a time-duration, from the following list (long form/short form):
|
|
// - second/s
|
|
// - minute/m
|
|
// - hour/h
|
|
// - day/d
|
|
// - week/w
|
|
// - month/M
|
|
// - year/y
|
|
// For example, possible input values, and their result:
|
|
// - "3d" or "3day" --> three days --> time.Now().UTC().Add(-3 * 24 * time.Hour)
|
|
// - "2m" or "2minute" --> two minutes --> time.Now().UTC().Add(-2 * time.Minute)
|
|
// - "1w" or "1week" --> one week --> time.Now().UTC().Add(-7 * 24 * time.Hour)
|
|
// - "30s" or "30second" --> thirty seconds --> time.Now().UTC().Add(-30 * time.Second)
|
|
// Note: Duration strings are case-sensitive, and should be used as mentioned above only.
|
|
// Limitation: Value of numerical multiplier, X should be in b/w 0 - 1e6 (1 million), boundary values excluded i.e.
|
|
// 0 < X < 1e6. Also, the maximum time in the past can be 1 January 1970 00:00:00 UTC (epoch time),
|
|
// so giving "1000y" will result in epoch time.
|
|
func parseTimeRange(timeRange string, now time.Time) (time.Time, error) {
|
|
match, err := regexp.MatchString(defaultDateTimeRangeShortRE, timeRange)
|
|
if !match { // fallback on to check if it's of longer notation
|
|
_, err = regexp.MatchString(defaultDateTimeRangeLongRE, timeRange)
|
|
}
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
|
|
re, _ := regexp.Compile(defaultDateTimeRangeNum)
|
|
idx := re.FindStringSubmatchIndex(timeRange)
|
|
if idx == nil {
|
|
return time.Time{}, fmt.Errorf("cannot parse timeRange %s", timeRange)
|
|
}
|
|
|
|
num, err := strconv.Atoi(timeRange[idx[0]:idx[1]])
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("cannot parse timeRange %s", timeRange)
|
|
}
|
|
if num >= 1e6 {
|
|
return time.Time{}, fmt.Errorf("invalid time-duation multiplier %d, allowed range is 0 < multiplier < 1000000", num)
|
|
}
|
|
|
|
dur, err := parseTimeDuration(timeRange[idx[1]:])
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("cannot parse timeRange %s", timeRange)
|
|
}
|
|
|
|
res := now.Add(time.Duration(-num) * dur) // using server's local timezone
|
|
epochTime := time.Unix(0, 0).UTC()
|
|
if res.Before(epochTime) {
|
|
res = epochTime
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// parseTimeDuration parses the given time duration in either short or long convention
|
|
// and returns the time.Duration
|
|
// Valid values (long notation/short notation):
|
|
// - second/s
|
|
// - minute/m
|
|
// - hour/h
|
|
// - day/d
|
|
// - week/w
|
|
// - month/M
|
|
// - year/y
|
|
// NOTE: the input "duration" is case-sensitive
|
|
func parseTimeDuration(duration string) (dur time.Duration, err error) {
|
|
switch duration {
|
|
case "s", "second":
|
|
dur = time.Second
|
|
case "m", "minute":
|
|
dur = time.Minute
|
|
case "h", "hour":
|
|
dur = time.Hour
|
|
case "d", "day":
|
|
dur = day
|
|
case "w", "week":
|
|
dur = week
|
|
case "M", "month":
|
|
dur = month
|
|
case "y", "year":
|
|
dur = year
|
|
default:
|
|
err = fmt.Errorf("unknown time duration %s", duration)
|
|
}
|
|
return
|
|
}
|
|
|
|
func newContext(c *cli.Context) (context.Context, context.CancelFunc) {
|
|
return newContextWithTimeout(c, defaultContextTimeout)
|
|
}
|
|
|
|
func newContextWithTimeout(c *cli.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
|
|
if c.IsSet(FlagContextTimeout) {
|
|
timeout = time.Duration(c.Int(FlagContextTimeout)) * time.Second
|
|
}
|
|
|
|
return context.WithTimeout(context.Background(), timeout)
|
|
}
|
|
|
|
func stringToEnum(search string, candidates map[string]int32) (int32, error) {
|
|
if search == "" {
|
|
return 0, nil
|
|
}
|
|
|
|
var candidateNames []string
|
|
for key, value := range candidates {
|
|
if strings.EqualFold(key, search) {
|
|
return value, nil
|
|
}
|
|
candidateNames = append(candidateNames, key)
|
|
}
|
|
|
|
return 0, fmt.Errorf("could not find corresponding candidate for %s. Possible candidates: %q", search, candidateNames)
|
|
}
|
|
|
|
// prompt will show input msg, then waiting user input y/yes to continue
|
|
func prompt(msg string, autoConfirm bool) {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
fmt.Print(msg, " ")
|
|
var text string
|
|
if autoConfirm {
|
|
text = "y"
|
|
fmt.Print("y")
|
|
} else {
|
|
text, _ = reader.ReadString('\n')
|
|
}
|
|
fmt.Println()
|
|
|
|
textLower := strings.ToLower(strings.TrimRight(text, "\n"))
|
|
if textLower != "y" && textLower != "yes" {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// paginate creates an interactive CLI mode to control the printing of items
|
|
func paginate[V any](c *cli.Context, paginationFn collection.PaginationFn[V], pageSize int) error {
|
|
more := c.Bool(FlagMore)
|
|
isTableView := !c.Bool(FlagPrintJSON)
|
|
iter := collection.NewPagingIterator(paginationFn)
|
|
|
|
var pageItems []interface{}
|
|
for iter.HasNext() {
|
|
item, err := iter.Next()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pageItems = append(pageItems, item)
|
|
if len(pageItems) == pageSize || !iter.HasNext() {
|
|
if isTableView {
|
|
if err := printTable(pageItems); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
prettyPrintJSONObject(pageItems)
|
|
}
|
|
|
|
if !more || !showNextPage() {
|
|
break
|
|
}
|
|
pageItems = pageItems[:0]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func printTable(items []interface{}) error {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
e := reflect.ValueOf(items[0])
|
|
for e.Type().Kind() == reflect.Ptr {
|
|
e = e.Elem()
|
|
}
|
|
|
|
var fields []string
|
|
t := e.Type()
|
|
for i := 0; i < e.NumField(); i++ {
|
|
fields = append(fields, t.Field(i).Name)
|
|
}
|
|
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.SetBorder(false)
|
|
table.SetColumnSeparator("|")
|
|
table.SetHeader(fields)
|
|
table.SetHeaderLine(false)
|
|
for i := 0; i < len(items); i++ {
|
|
item := reflect.ValueOf(items[i])
|
|
for item.Type().Kind() == reflect.Ptr {
|
|
item = item.Elem()
|
|
}
|
|
var columns []string
|
|
for j := 0; j < len(fields); j++ {
|
|
col := item.Field(j)
|
|
columns = append(columns, fmt.Sprintf("%v", col.Interface()))
|
|
}
|
|
table.Append(columns)
|
|
}
|
|
table.Render()
|
|
table.ClearRows()
|
|
|
|
return nil
|
|
}
|
|
|
|
func showNextPage() bool {
|
|
fmt.Printf("Press %s to show next page, press %s to quit: ",
|
|
color.GreenString("Enter"), color.RedString("any other key then Enter"))
|
|
var input string
|
|
_, _ = fmt.Scanln(&input)
|
|
return strings.Trim(input, " ") == ""
|
|
}
|
|
|
|
func getNamespaceID(c *cli.Context, nsName namespace.Name) (namespace.ID, error) {
|
|
wfClient := cFactory.WorkflowClient(c)
|
|
|
|
ctx, cancel := newContext(c)
|
|
defer cancel()
|
|
|
|
nsResponse, err := wfClient.DescribeNamespace(ctx, &workflowservice.DescribeNamespaceRequest{
|
|
Namespace: nsName.String(),
|
|
})
|
|
if err != nil {
|
|
return namespace.EmptyID, err
|
|
}
|
|
|
|
return namespace.ID(nsResponse.NamespaceInfo.GetId()), nil
|
|
}
|