hzDocs

Using is Detectors

is.inDeveloping()?

介绍

hedzr.is 是一个以环境检测函数为主的基础支持库。

is provides numerous detectors for checking the states of environment (build, executive, ...).

is 中提供了大量 Golang 运行环境检测工具,有的独立工作,有的需要配合 go build tags 协同工作。 总的来说,它们可以帮助你更好地确定当前运行环境,让你的代码能够据此进行适配。

Features

  • is.State(which) bool: the universal detector entry - via RegisterStateGetter(state string, getter func() bool) to add your own ones. Since v0.5.11
  • is.Env() holds a global struct for CLI app basic states, such as: verbose/quiet/debug/trace....
    • DebugMode/DebugLevel, TraceMode/TraceLevel, ColorMode, ...
  • is.InDebugging() bool, is.InTesting() bool, and is.InTracing() bool, ....
  • is.DebugBuild() bool.
  • is.K8sBuild() bool, is.DockerBuild() bool, ....
  • is.ColoredTty() bool, ....
  • is.Color() to get an indexer for the functions in our term/color subpackage, ...
  • Terminal Colorizer, Detector, unescape tools.
  • stringtool: RandomStringPure, case-converters ...
  • basics: closable, closer, signals.
    • easier Press any key to exit... prompt: is.Signals().Catch()
  • exec: Run, RunWithOutput, Sudo, ...
  • go1.23.7+ required since v0.7.0
  • go 1.22.7+ required

To using environment detecting utilities better and smoother, some terminal (and stringtool, basics) tools are bundled together.

Since v0.6.0, is.InDebugging() checks if the running process' parent is dlv. The old DebugMode and DebugBuild are still work:

  • InDebugging: checks this process is being debugged by dlv.
  • DebugBuild: -tags=delve is set at building.
  • DebugMode: --debug is specified at command line.

分类

基础环境

  • InDebugging: checks this process is being debugged by dlv.

  • DebugBuild: -tags=delve is set at building.

  • DebugMode: --debug is specified at command line.

  • is.InTracing / InTestingT

  • is.InTesting / InTestingT

  • is.InDevelopingTime

  • is.InVscodeTerminal

  • is.InK8s

  • is.InIstio

  • is.InDocker / InDockerEnvSimple

  • Build (need -tags=xx)

    • is.K8sBuild: need -tags=k8s present
    • is.IstioBuild: need -tags=istio present
    • is.DockerBuild: need -tags=docker present
    • is.VerboseBuild: need -tags=verbose present
    • is.DebugBuild: need -tags=delve present
    • buildtags.IsBuildTagExists

States / Env

  • VerboseModeEnabled
  • GetVerboseLevel / SetVerboseMode / SetVerboseLevel
  • QuietModeEnabled
  • GetQuietLevel / SetQuietMode / SetQuietLevel
  • NoColorMode
  • GetNoColorLevel / SetNoColorMode / SetNoColorLevel
  • DebugMode
  • GetDebugLevel / SetDebugMode / SetDebugLevel
  • Tracing
  • TraceMode
  • GetTraceLevel / SetTraceMode / SetTraceLevel

Terminal / Tty

  • is.Terminal(file)
  • is.TerminalFd(fd)
  • is.Tty(wr)
  • is.ColoredTty(wr)
  • is.AnsiEscaped(s) (IsTtyEscaped(s))
  • StripEscapes(s)
  • ReadPassword
  • GetTtySize
  • is.GetTtySizeByName(filename) (cols,rows,err)
  • is.GetTtySizeByFile(file) (cols,rows,err)
  • is.GetTtySizeByFd(fd) (cols,rows,err)
  • StartupByDoubleClick() bool

Basics

  • closers
    • Peripheral, Closable, Closer
    • RegisterClosable
    • RegisterClosers
    • RegisterCloseFns
  • is.Signals().Catcher()
  • is.FileExists(filepath)
  • is.ToBool, StringToBool

utilities

  • dir subpackage
  • exec subpackage
  • term/color subpackage
    • escaping tools: GetCPT()/GetCPTC()/GetCPTNC()
    • Highlight, Dimf, Text, Dim, ToDim, ToHighlight, ToColor, ...

Usages

package main
 
import (
    "context"
    "fmt"
    "log/slog"
    "os"
    "sync"
    "time"
 
    "github.com/hedzr/is"
    "github.com/hedzr/is/basics"
    "github.com/hedzr/is/term/color"
)
 
func main() {
    defer basics.Close()
 
    is.RegisterStateGetter("custom", func() bool { return is.InVscodeTerminal() })
 
    println(is.InTesting())
    println(is.State("in-testing"))
    println(is.State("custom")) // detects a state with custom detector
    println(is.Env().GetDebugLevel())
    if is.InDebugMode() {
        slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: true, Level: slog.LevelDebug})))
    }
 
    // or:
    //    is.Color().GetColorTranslator().Translate("<b>bold</b>")
    fmt.Printf("%v", color.GetCPT().Translate(`<code>code</code> | <kbd>CTRL</kbd>
        <b>bold / strong / em</b>
        <i>italic / cite</i>
        <u>underline</u>
        <mark>inverse mark</mark>
        <del>strike / del </del>
        <font color="green">green text</font>
`, color.FgDefault))
 
    ctx, cancel := context.WithCancel(context.Background())
    catcher := is.Signals().Catch()
    catcher.
        WithPrompt("Press CTRL-C to quit...").
        WithOnLoop(dbStarter, cacheStarter, mqStarter).
        WithOnSignalCaught(func(sig os.Signal, wg *sync.WaitGroup) {
            println()
            slog.Info("signal caught", "sig", sig)
            cancel() // cancel user's loop, see Wait(...)
        }).
        Wait(func(stopChan chan<- os.Signal, wgDone *sync.WaitGroup) {
            slog.Debug("entering looper's loop...")
            go func() {
                // to terminate this app after a while automatically:
                time.Sleep(10 * time.Second)
                stopChan <- os.Interrupt
            }()
            <-ctx.Done()  // waiting until any os signal caught
            wgDone.Done() // and complete myself
        })
}
 
func dbStarter(stopChan chan<- os.Signal, wgDone *sync.WaitGroup) {
    // initializing database connections...
    // ...
    wgDone.Done()
}
 
func cacheStarter(stopChan chan<- os.Signal, wgDone *sync.WaitGroup) {
    // initializing redis cache connections...
    // ...
    wgDone.Done()
}
 
func mqStarter(stopChan chan<- os.Signal, wgDone *sync.WaitGroup) {
    // initializing message queue connections...
    // ...
    wgDone.Done()
}

Result is similar with:

image-20240113071930661

NOTE that is.Signals().Catch() will produce a prompt and enter a infinite loop to wait for user's keystroke pressed.

Build Tags

Some functions want special buildtags presented. These are including:

  • verbose: See VerboseBuild, ...
  • delve: See DebugBuild, ...
  • k8s: See K8sBuild
  • istio: See IstioBuild
  • docker: See DockerBuild
  • ...
  • buildtags.IsBuildTagExists(tag) bool

Colorizes

The test codes:

import "github.com/hedzr/is/term/color"
 
func TestGetCPT(t *testing.T) {
  t.Logf("%v", color.GetCPT().Translate(`<code>code</code> | <kbd>CTRL</kbd>
    <b>bold / strong / em</b>
    <i>italic / cite</i>
    <u>underline</u>
    <mark>inverse mark</mark>
    <del>strike / del </del>
    <font color="green">green text</font>
    `, color.FgDefault))
}

Result:

image-20231107100150520

And more:

func TestStripLeftTabs(t *testing.T) {
t.Logf("%v", color.StripLeftTabs(`
 
        <code>code</code>
    NC Cool
     But it's tight.
      Hold On!
    Hurry Up.
    `))
}
 
func TestStripHTMLTags(t *testing.T) {
t.Logf("%v", color.StripHTMLTags(`
 
        <code>code</code>
    NC Cool
     But it's tight.
      Hold On!
    Hurry Up.
    `))
}

cmdr 相集成

Closers

is 提供一个子包 closers 来抽象你的基础设施。

type Peripheral interface {
  Close()
}

一个基础设施意味着一个需要在应用程序结束前实施其关闭操作的对象,可以是外部资源如数据库连接等,也可以是一个文件句柄。

在你的 app 中,可以使用 RegisterPeripheral 注册一个基础设施,并提供 defer 调用以便关闭所有这些对象。

func main() {
  defer is.Closers().Close()
 
  // ...RegisterPeripheral yours
}

The Closers() collects all closable objects and allow shutting down them at once.

package main
 
import (
    "os"
 
    "github.com/hedzr/is/basics"
)
 
type redisHub struct{}
 
func (s *redisHub) Close() {
    // close the connections to redis servers
    println("redis connections closed")
}
 
func main() {
    defer basics.Close()
 
    tmpFile, _ := os.CreateTemp(os.TempDir(), "1*.log")
    basics.RegisterClosers(tmpFile)
 
    basics.RegisterCloseFn(func() {
        // do some shutdown operations here
        println("close single functor")
    })
 
    basics.RegisterPeripheral(&redisHub{})
}

with cmdr

cmdr 在它的工作流程中会自动执行相似的调用以便关闭已经注册的基础设施,因此 is + cmdr 可以更无缝地协同工作。

具体实施可以参考:

Signals

Signals() could catch OS signals and entering a infinite loop.

is.Signals() 能够拦截 POSIX 信号并回调你的响应函数。

For example, a tcp server could be:

package main
 
import (
    "context"
    "os"
    "sync"
 
    "github.com/hedzr/go-socketlib/net"
    "github.com/hedzr/is"
    logz "github.com/hedzr/logg/slog"
)
 
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
 
    logger := logz.New("new-dns")
    server := net.NewServer(":7099",
        net.WithServerOnListening(func(ss net.Server, l stdnet.Listener) {
            go runClient(ctx, ss, l)
        }),
        net.WithServerLogger(logger.WithSkip(1)),
    )
    defer server.Close()
 
    // make a catcher so that it can catch ths signals,
    catcher := is.Signals().Catch()
    catcher.
        // WithVerboseFn(func(msg string, args ...any) {
        //     logz.WithSkip(2).Verbose(fmt.Sprintf("[verbose] %s", fmt.Sprintf(msg, args...)))
        // }).
        WithOnSignalCaught(func(sig os.Signal, wg *sync.WaitGroup) {
            println()
            // logz.Debug("signal caught", "sig", sig)
            if err := server.Shutdown(); err != nil {
                logz.Error("server shutdown error", "err", err)
            }
            cancel()
        }).
        WaitFor(func(closer func()) {
            logz.Debug("entering looper's loop...")
 
            server.WithOnShutdown(func(err error, ss net.Server) { closer() })
            err := server.ListenAndServe(ctx, nil)
            if err != nil {
                logz.Fatal("server serve failed", "err", err)
            }
        })
}
 
func runClient(ctx context.Context, ss net.Server, l stdnet.Listener) {
    c := net.NewClient()
 
    if err := c.Dial("tcp", ":7099"); err != nil {
        logz.Fatal("connecting to server failed", "err", err, "server-endpoint", ":7099")
    }
    logz.Info("[client] connected", "server.addr", c.RemoteAddr())
    c.RunDemo(ctx)
}

WaitFor()

WaitFor() 回调函数体内,你可以启动你的服务。它们不必在 go routine 中被启动,而是可以直接放在 WaitFor() 回调函数体内。这是因为 Catcher() 将在一个 go routine 中运行你的回调函数。

所以对于 http server 来说,下面的做法是正确的:

  WaitFor(func(closer func()) {
    logz.Debug("entering looper's loop...")
 
    server.WithOnShutdown(func(err error, ss net.Server) { closer() })
    err := server.ListenAndServe(ctx, nil)
    if err != nil {
      logz.Fatal("server serve failed", "err", err)
    }
  })

假设你在其他地方启动了服务,并且通过 ctx cancel() 来管理那些服务的生存周期,那么 WaitFor 回调函数可以这么写:

  WaitFor(func(closer func()) {
    defer closer()
    for {
      select {
      case <-ticker.C:
        wakeupForTask();
      case <-ctx.Done():
        return
      }
    }
  })

当全局的 cancellable ctx 的 cancel() 函数被调用时,你的其他服务将会侦听 ctx.Done() 信号并 Shutdown,而上面的 WaitFor() 回调函数体也会结束自己,并调用 closer() 来清理 Catcher() 自身。

Howto terminate myself after a while

有时候我们可能需要提示用户等待 10 秒钟(10s)然后自动结束等待并退出程序。 这个需求可以用 Catch() 来实现:

	ctx, cancel := context.WithCancel(context.Background())
	catcher := is.Signals().Catch()
	catcher.
		WithPrompt("Press CTRL-C to quit...").
		WithOnSignalCaught(func(sig os.Signal, wg *sync.WaitGroup) {
			println()
			// slog.Info("signal caught", "sig", sig)
			cancel() // cancel user's loop, see Wait(...)
		}).
		WaitFor(func(closer func()) {
			// slog.Debug("entering looper's loop...")
			go func() {
				// to terminate this app after a while automatically:
				time.Sleep(10 * time.Second)
				// closer will send a os.SigInt to cause `WithOnSignalCaught`
        // calling `cancel()`
				closer()
			}()
			<-ctx.Done() // waiting until any os signal caught
		})

尽管这个方案看起来稍微有些笨重繁复,但事实上并非如此,它工作起来相当轻巧,没有任何互锁机制导致迟滞。

当然,如果是单纯地需要一个简单地“Press any key to continue...”,可以使用 is.PressAnyKeyToContinue(in io.Reader, msg ...string) (input string) 或者 is.PressEnterToContinue(in io.Reader, msg ...string) (input string) 来达成目的。它们仅仅使用 bufio.Fscanf() 或者 bufio.NewReader(in).ReadBytes('\n'),一定比 Catch() 方案轻巧——除非你也同时需要对 os.Signal 进行捕俘。你应该提供 os.Stdin 作为其参数。

额外的话题

:end:

How is this guide?

Edit on GitHub

Last updated on

On this page