first version
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
config/*
|
||||||
|
runners/*
|
||||||
|
output/*
|
||||||
136
README.md
136
README.md
@@ -1,3 +1,137 @@
|
|||||||
# dengitool
|
# dengitool
|
||||||
|
|
||||||
horrible scripting tool for microslop bindows
|
simple but horrible scripting tool focused on microslop bindows servers written in go
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## example instalation
|
||||||
|
|
||||||
|
> `dtool.exe` is portable, you can _use any directory_
|
||||||
|
|
||||||
|
1. Create folder and copy `dtool.exe`. For example in `C:\dtooll`
|
||||||
|
1. Add to env **`PATH`** your folder
|
||||||
|
1. That's it! Open a terminal and try to run `dtool -h`
|
||||||
|
|
||||||
|
## setup app
|
||||||
|
|
||||||
|
example tree
|
||||||
|
|
||||||
|
```
|
||||||
|
config
|
||||||
|
└───<application>
|
||||||
|
└───<module>
|
||||||
|
└───run
|
||||||
|
customscript.ps1
|
||||||
|
check.ps1
|
||||||
|
config.yml
|
||||||
|
fix.ps1
|
||||||
|
restart.ps1
|
||||||
|
start.ps1
|
||||||
|
stop.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
> **TIP:** you can leave only scripts that you need
|
||||||
|
|
||||||
|
you can write scripts **in any language you want** which is supported by [built-in script runners](#built-in), OR you can write **your own** [custom runner](#custom-runners)
|
||||||
|
|
||||||
|
#### check
|
||||||
|
|
||||||
|
for checkking status you can use either your script or you can write in config.yml `check-url` [(details)](#config)
|
||||||
|
|
||||||
|
#### restart
|
||||||
|
|
||||||
|
you can define restart script or it would use start and stop
|
||||||
|
|
||||||
|
#### config
|
||||||
|
|
||||||
|
currently there are config for `application`, example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: my-application
|
||||||
|
```
|
||||||
|
|
||||||
|
and for `module`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: my-module
|
||||||
|
#
|
||||||
|
check-url: https://example.com
|
||||||
|
# or
|
||||||
|
check-url: https://example.com 200
|
||||||
|
# or even
|
||||||
|
check-url: https://example.com 200 Test%20Body%20Text
|
||||||
|
# or don't define at all
|
||||||
|
#
|
||||||
|
```
|
||||||
|
|
||||||
|
for now that's it.
|
||||||
|
|
||||||
|
## usage
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
dtool [application] [action] [module] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for dtool
|
||||||
|
--list list configured applications and modules
|
||||||
|
--list-runners list configured script runners
|
||||||
|
--silent suppress all output
|
||||||
|
--verbose enable debug output
|
||||||
|
```
|
||||||
|
|
||||||
|
you can use `everything` for `application`
|
||||||
|
|
||||||
|
just `dtool` is alias for `dtool everything check`
|
||||||
|
|
||||||
|
#### actions
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ------------------------------ | ------------------------------------------------------ |
|
||||||
|
| `check/fix/restart/start/stop` | runs it's script [(or check have url defined)](#check) |
|
||||||
|
| `run [script name]` | runs script in `run` subdirectory |
|
||||||
|
|
||||||
|
## runners
|
||||||
|
|
||||||
|
dengitool automaticly detects available script extension
|
||||||
|
|
||||||
|
#### built-in
|
||||||
|
|
||||||
|
| name | extension |
|
||||||
|
| ------------------ | --------- |
|
||||||
|
| Windows Executable | `.exe` |
|
||||||
|
| PowerShell Script | `.ps1` |
|
||||||
|
| Python | `.py` |
|
||||||
|
| CMD Batch File | `.bat` |
|
||||||
|
| NodeJS | `.js` |
|
||||||
|
|
||||||
|
#### custom runners
|
||||||
|
|
||||||
|
you can create your own runners
|
||||||
|
|
||||||
|
for this, you need to create a .yml file at folder `runners` near dtool.exe
|
||||||
|
|
||||||
|
you can **override** script for built-in extension
|
||||||
|
|
||||||
|
**here is an example:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# runners/bun.yml
|
||||||
|
name: My Bun Runner
|
||||||
|
extension: .ts
|
||||||
|
runner: bun run %v
|
||||||
|
```
|
||||||
|
|
||||||
|
> `%v` - absolute script path
|
||||||
|
|
||||||
|
now you can see it in `dtool --list-runners`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# thanks
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
182
appmodules.go
Normal file
182
appmodules.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppModule struct {
|
||||||
|
Name string
|
||||||
|
Folder string
|
||||||
|
CheckS string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppModuleConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
CheckUrl *string `yaml:"check-url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Script struct {
|
||||||
|
Name string
|
||||||
|
AbsPath string
|
||||||
|
Runner ScriptRunner
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Script) Run() (string, error) {
|
||||||
|
args := runnerArgs(s.Runner.Runner, s.AbsPath)
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", errors.New("runner command is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
return string(output), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func runnerArgs(template string, scriptPath string) []string {
|
||||||
|
fields := strings.Fields(template)
|
||||||
|
for i, field := range fields {
|
||||||
|
fields[i] = strings.ReplaceAll(field, "%v", scriptPath)
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AppModule) HasScript(name string, subfolder *string) (bool, *Script) {
|
||||||
|
folder := m.Folder
|
||||||
|
if subfolder != nil {
|
||||||
|
folder = filepath.Join(folder, *subfolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range runners {
|
||||||
|
ext := r.Extension
|
||||||
|
if ext != "" && !strings.HasPrefix(ext, ".") {
|
||||||
|
ext = "." + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptPath := filepath.Join(folder, name+ext)
|
||||||
|
if fileExists(scriptPath) {
|
||||||
|
abs, err := filepath.Abs(scriptPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, &Script{Name: name, AbsPath: abs, Runner: r}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
func (m *AppModule) Check() (bool, string, error) {
|
||||||
|
if strings.HasPrefix(m.CheckS, "URL ") {
|
||||||
|
check := strings.Split(m.CheckS, " ")[1:]
|
||||||
|
if len(check) < 1 || len(check) > 3 {
|
||||||
|
return false, "", errors.New("check url is bad")
|
||||||
|
}
|
||||||
|
checkUrl := check[0]
|
||||||
|
code := ""
|
||||||
|
if len(check) > 1 {
|
||||||
|
code = check[1]
|
||||||
|
}
|
||||||
|
body := ""
|
||||||
|
if len(check) > 2 {
|
||||||
|
dBody, err := url.QueryUnescape(check[2])
|
||||||
|
if err != nil {
|
||||||
|
return false, "", errors.New("check body decode failed")
|
||||||
|
}
|
||||||
|
body = dBody
|
||||||
|
}
|
||||||
|
|
||||||
|
cCode, cBody, err := fetch(checkUrl, defaultFetchTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
if code != "" && strconv.Itoa(cCode) != code {
|
||||||
|
return false, "", fmt.Errorf("code is different: expected %s, got %d", code, cCode)
|
||||||
|
}
|
||||||
|
if body != "" && body != cBody {
|
||||||
|
return false, "", errors.New("body is different")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, "", nil
|
||||||
|
|
||||||
|
} else {
|
||||||
|
exists, script := m.HasScript("check", nil)
|
||||||
|
if exists && script != nil {
|
||||||
|
output, err := script.Run()
|
||||||
|
if err != nil {
|
||||||
|
return false, output, err
|
||||||
|
} else {
|
||||||
|
return true, output, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false, "", errors.New("check script not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AppModule) Start() (string, error) {
|
||||||
|
exists, script := m.HasScript("start", nil)
|
||||||
|
if exists && script != nil {
|
||||||
|
return script.Run()
|
||||||
|
} else {
|
||||||
|
return "", errors.New("start script not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AppModule) Stop() (string, error) {
|
||||||
|
exists, script := m.HasScript("stop", nil)
|
||||||
|
if exists && script != nil {
|
||||||
|
return script.Run()
|
||||||
|
} else {
|
||||||
|
return "", errors.New("stop script not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AppModule) Restart() (string, error) {
|
||||||
|
exists, script := m.HasScript("restart", nil)
|
||||||
|
if exists && script != nil {
|
||||||
|
return script.Run()
|
||||||
|
} else {
|
||||||
|
stopOutput, err := m.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return stopOutput, err
|
||||||
|
}
|
||||||
|
startOutput, err := m.Start()
|
||||||
|
return joinOutput(stopOutput, startOutput), err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AppModule) Fix() (string, error) {
|
||||||
|
exists, script := m.HasScript("fix", nil)
|
||||||
|
|
||||||
|
if exists && script != nil {
|
||||||
|
return script.Run()
|
||||||
|
} else {
|
||||||
|
return m.Restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AppModule) Run(scriptN string) (string, error) {
|
||||||
|
var sub = "run"
|
||||||
|
exists, script := m.HasScript(scriptN, &sub)
|
||||||
|
|
||||||
|
if exists && script != nil {
|
||||||
|
return script.Run()
|
||||||
|
} else {
|
||||||
|
return "", errors.New("run script not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
Name string
|
||||||
|
Folder string
|
||||||
|
AppModules []AppModule
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
}
|
||||||
13
go.mod
Normal file
13
go.mod
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module git.ktkz.ru/katamaz/dengitool
|
||||||
|
|
||||||
|
go 1.26.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
)
|
||||||
12
go.sum
Normal file
12
go.sum
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
52
logger.go
Normal file
52
logger.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
silentMode bool
|
||||||
|
verboseMode bool
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
colorRed = "\x1b[31m"
|
||||||
|
colorReset = "\x1b[0m"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logger struct{}
|
||||||
|
|
||||||
|
func (logger) Info(args ...any) {
|
||||||
|
if silentMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger) Debug(args ...any) {
|
||||||
|
if silentMode || !verboseMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger) Println(args ...any) {
|
||||||
|
if silentMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger) Error(args ...any) {
|
||||||
|
fmt.Fprint(os.Stderr, colorRed)
|
||||||
|
fmt.Fprintln(os.Stderr, args...)
|
||||||
|
fmt.Fprint(os.Stderr, colorReset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger) Fatalln(args ...any) {
|
||||||
|
log.Error(args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var log = logger{}
|
||||||
413
main.go
Normal file
413
main.go
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
//
|
||||||
|
// project dengitool
|
||||||
|
// Copyright (C) 2026 katamaz
|
||||||
|
//
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var runners = GetScriptsRunners()
|
||||||
|
|
||||||
|
var silent, verbose, listModules, listRunners bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&silent, "silent", false, "suppress all output")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "enable debug output")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&listModules, "list", false, "list configured applications and modules")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&listRunners, "list-runners", false, "list configured script runners")
|
||||||
|
|
||||||
|
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
|
// silent wins
|
||||||
|
initLogger(silent, verbose && !silent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(folderExists(("./config/"))) {
|
||||||
|
log.Info("No config folder found, creating new...")
|
||||||
|
if err := os.Mkdir("./config/", 0750); err != nil {
|
||||||
|
log.Fatalln("Failed to create config folder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initLogger(silent bool, verbose bool) {
|
||||||
|
silentMode = silent
|
||||||
|
verboseMode = verbose
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "dtool [application] [action] [module]",
|
||||||
|
Short: "DengiTool",
|
||||||
|
Long: "DengiTool CLI",
|
||||||
|
SilenceErrors: true,
|
||||||
|
SilenceUsage: true,
|
||||||
|
Args: cobra.ArbitraryArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runCLI(args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCLI(args []string) error {
|
||||||
|
if listModules {
|
||||||
|
return printModules()
|
||||||
|
}
|
||||||
|
if listRunners {
|
||||||
|
return printRunners()
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := parseRequest(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps := listApplications()
|
||||||
|
if len(apps) == 0 {
|
||||||
|
log.Println("No apps found\nNothing to do")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
targets, err := resolveTargets(apps, request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return errors.New("no modules matched")
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []actionResult
|
||||||
|
currentApp := ""
|
||||||
|
for i, target := range targets {
|
||||||
|
if target.app.Folder != currentApp {
|
||||||
|
currentApp = target.app.Folder
|
||||||
|
if !silent {
|
||||||
|
log.Println("▸ " + displayName(target.app.Name, filepath.Base(target.app.Folder)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, runModuleAction(target.app, target.module, request, treePrefix(i, targets)))
|
||||||
|
}
|
||||||
|
|
||||||
|
showSummary(results)
|
||||||
|
if failed := countFailed(results); failed > 0 {
|
||||||
|
return fmt.Errorf("%d of %d action(s) failed", failed, len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printModules() error {
|
||||||
|
apps := listApplications()
|
||||||
|
if len(apps) == 0 {
|
||||||
|
log.Println("No apps found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, app := range apps {
|
||||||
|
log.Println(displayName(app.Name, filepath.Base(app.Folder)))
|
||||||
|
if len(app.AppModules) == 0 {
|
||||||
|
log.Println(" (no modules)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, module := range app.AppModules {
|
||||||
|
log.Println(" " + displayName(module.Name, filepath.Base(module.Folder)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRunners() error {
|
||||||
|
if len(runners) == 0 {
|
||||||
|
log.Println("No runners found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, runner := range runners {
|
||||||
|
log.Println(fmt.Sprintf("%s %s", runner.Name, runner.Extension))
|
||||||
|
if verbose {
|
||||||
|
log.Println(" " + runner.Runner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type cliRequest struct {
|
||||||
|
application string
|
||||||
|
action string
|
||||||
|
module string
|
||||||
|
script string
|
||||||
|
}
|
||||||
|
|
||||||
|
type moduleTarget struct {
|
||||||
|
app Application
|
||||||
|
module AppModule
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionResult struct {
|
||||||
|
label string
|
||||||
|
output string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRequest(args []string) (cliRequest, error) {
|
||||||
|
request := cliRequest{application: "everything", action: "check"}
|
||||||
|
if len(args) == 0 {
|
||||||
|
return request, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 4 {
|
||||||
|
return request, usageError()
|
||||||
|
}
|
||||||
|
|
||||||
|
request.application = args[0]
|
||||||
|
if len(args) > 1 {
|
||||||
|
request.action = args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validAction(request.action) {
|
||||||
|
return request, fmt.Errorf("unknown action %q; valid actions: %s", request.action, strings.Join(validActions(), ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.action == "run" {
|
||||||
|
if len(args) < 3 {
|
||||||
|
return request, errors.New("usage: dtool [application] run [script] [module]")
|
||||||
|
}
|
||||||
|
request.script = args[2]
|
||||||
|
if len(args) > 3 {
|
||||||
|
request.module = args[3]
|
||||||
|
}
|
||||||
|
return request, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 2 {
|
||||||
|
request.module = args[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
return request, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validAction(action string) bool {
|
||||||
|
for _, valid := range validActions() {
|
||||||
|
if action == valid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func validActions() []string {
|
||||||
|
return []string{"check", "start", "stop", "restart", "fix", "run"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usageError() error {
|
||||||
|
return errors.New("usage: dtool [application] [action] [module]\n dtool [application] run [script] [module]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveTargets(apps []Application, request cliRequest) ([]moduleTarget, error) {
|
||||||
|
var selected []Application
|
||||||
|
if request.application == "everything" {
|
||||||
|
if request.module != "" {
|
||||||
|
return nil, errors.New(`"everything" runs across all modules; omit the module argument`)
|
||||||
|
}
|
||||||
|
selected = apps
|
||||||
|
} else {
|
||||||
|
app, ok := findApplication(apps, request.application)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("application %q not found; available applications: %s", request.application, applicationNames(apps))
|
||||||
|
}
|
||||||
|
selected = []Application{app}
|
||||||
|
}
|
||||||
|
|
||||||
|
var targets []moduleTarget
|
||||||
|
for _, app := range selected {
|
||||||
|
if request.module == "" {
|
||||||
|
for _, module := range app.AppModules {
|
||||||
|
targets = append(targets, moduleTarget{app: app, module: module})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
module, ok := findModule(app, request.module)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("module %q not found in %q; available modules: %s", request.module, app.Name, moduleNames(app))
|
||||||
|
}
|
||||||
|
targets = append(targets, moduleTarget{app: app, module: module})
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findApplication(apps []Application, name string) (Application, bool) {
|
||||||
|
for _, app := range apps {
|
||||||
|
if sameName(app.Name, name) || sameName(filepath.Base(app.Folder), name) {
|
||||||
|
return app, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Application{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func findModule(app Application, name string) (AppModule, bool) {
|
||||||
|
for _, module := range app.AppModules {
|
||||||
|
if sameName(module.Name, name) || sameName(filepath.Base(module.Folder), name) {
|
||||||
|
return module, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AppModule{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameName(left string, right string) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(left), strings.TrimSpace(right))
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationNames(apps []Application) string {
|
||||||
|
names := make([]string, 0, len(apps))
|
||||||
|
for _, app := range apps {
|
||||||
|
names = append(names, displayName(app.Name, filepath.Base(app.Folder)))
|
||||||
|
}
|
||||||
|
return strings.Join(names, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func moduleNames(app Application) string {
|
||||||
|
names := make([]string, 0, len(app.AppModules))
|
||||||
|
for _, module := range app.AppModules {
|
||||||
|
names = append(names, displayName(module.Name, filepath.Base(module.Folder)))
|
||||||
|
}
|
||||||
|
if len(names) == 0 {
|
||||||
|
return "(none)"
|
||||||
|
}
|
||||||
|
return strings.Join(names, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayName(name string, folder string) string {
|
||||||
|
if sameName(name, folder) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s (%s)", name, folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runModuleAction(app Application, module AppModule, request cliRequest, prefix treeParts) actionResult {
|
||||||
|
label := fmt.Sprintf("%s/%s %s", app.Name, module.Name, request.action)
|
||||||
|
moduleLabel := fmt.Sprintf("%s %s", displayName(module.Name, filepath.Base(module.Folder)), request.action)
|
||||||
|
if request.action == "run" {
|
||||||
|
label = fmt.Sprintf("%s/%s run %s", app.Name, module.Name, request.script)
|
||||||
|
moduleLabel = fmt.Sprintf("%s run %s", displayName(module.Name, filepath.Base(module.Folder)), request.script)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !silent {
|
||||||
|
log.Println(prefix.branch + "⏳ " + moduleLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
output := ""
|
||||||
|
switch request.action {
|
||||||
|
case "check":
|
||||||
|
_, output, err = module.Check()
|
||||||
|
case "start":
|
||||||
|
output, err = module.Start()
|
||||||
|
case "stop":
|
||||||
|
output, err = module.Stop()
|
||||||
|
case "restart":
|
||||||
|
output, err = module.Restart()
|
||||||
|
case "fix":
|
||||||
|
output, err = module.Fix()
|
||||||
|
case "run":
|
||||||
|
output, err = module.Run(request.script)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unknown action %q", request.action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !silent {
|
||||||
|
printTreeError(prefix.branch + "✗ " + moduleLabel + ": " + err.Error())
|
||||||
|
printScriptOutput(prefix.continuation, output)
|
||||||
|
}
|
||||||
|
return actionResult{label: label, output: output, err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !silent {
|
||||||
|
log.Println(prefix.branch + "✓ " + moduleLabel)
|
||||||
|
printScriptOutput(prefix.continuation, output)
|
||||||
|
}
|
||||||
|
return actionResult{label: label, output: output}
|
||||||
|
}
|
||||||
|
|
||||||
|
type treeParts struct {
|
||||||
|
branch string
|
||||||
|
continuation string
|
||||||
|
}
|
||||||
|
|
||||||
|
func treePrefix(index int, targets []moduleTarget) treeParts {
|
||||||
|
if index == len(targets)-1 || targets[index+1].app.Folder != targets[index].app.Folder {
|
||||||
|
return treeParts{branch: "└─ ", continuation: " "}
|
||||||
|
}
|
||||||
|
return treeParts{branch: "├─ ", continuation: "│ "}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printScriptOutput(prefix string, output string) {
|
||||||
|
output = strings.TrimRight(output, "\r\n")
|
||||||
|
if output == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
log.Println(prefix + " " + strings.TrimRight(line, "\r"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTreeError(message string) {
|
||||||
|
fmt.Fprint(os.Stdout, colorRed)
|
||||||
|
fmt.Fprintln(os.Stdout, message)
|
||||||
|
fmt.Fprint(os.Stdout, colorReset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showSummary(results []actionResult) {
|
||||||
|
if silent {
|
||||||
|
for _, result := range results {
|
||||||
|
if result.err != nil {
|
||||||
|
log.Error("FAIL", result.label+":", result.err)
|
||||||
|
printScriptOutput("", result.output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
failed := countFailed(results)
|
||||||
|
if failed == 0 {
|
||||||
|
log.Println(fmt.Sprintf("Done: %d action(s) succeeded", len(results)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
if result.err != nil {
|
||||||
|
log.Debug("FAIL", result.label+":", result.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Error(fmt.Sprintf("Done: %d succeeded, %d failed", len(results)-failed, failed))
|
||||||
|
}
|
||||||
|
|
||||||
|
func countFailed(results []actionResult) int {
|
||||||
|
failed := 0
|
||||||
|
for _, result := range results {
|
||||||
|
if result.err != nil {
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failed
|
||||||
|
}
|
||||||
46
scriptsRunner.go
Normal file
46
scriptsRunner.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScriptRunner struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Extension string `yaml:"extension"`
|
||||||
|
Runner string `yaml:"runner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetScriptsRunners() []ScriptRunner {
|
||||||
|
runners := []ScriptRunner{}
|
||||||
|
|
||||||
|
if folderExists("./runners") {
|
||||||
|
entries, err := os.ReadDir("./runners/")
|
||||||
|
if err == nil {
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() && strings.HasSuffix(e.Name(), ".yml") {
|
||||||
|
runner, err := os.ReadFile("./runners/" + e.Name())
|
||||||
|
if err == nil {
|
||||||
|
var rnr ScriptRunner
|
||||||
|
if err := yaml.Unmarshal(runner, &rnr); err == nil {
|
||||||
|
runners = append(runners, rnr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRunners := []ScriptRunner{
|
||||||
|
{Name: "Windows Executable", Extension: ".exe", Runner: "%v"},
|
||||||
|
{Name: "PowerShell Script", Extension: ".ps1", Runner: "powershell -NoProfile -ExecutionPolicy Bypass -File %v"},
|
||||||
|
{Name: "Python", Extension: ".py", Runner: "python %v"},
|
||||||
|
{Name: "CMD Batch File", Extension: ".bat", Runner: "%v"},
|
||||||
|
{Name: "NodeJS", Extension: ".js", Runner: "node %v"},
|
||||||
|
}
|
||||||
|
runners = append(runners, defaultRunners...)
|
||||||
|
|
||||||
|
return runners
|
||||||
|
}
|
||||||
137
utils.go
Normal file
137
utils.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultFetchTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
func folderExists(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return info.IsDir()
|
||||||
|
}
|
||||||
|
func fileExists(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func quoteCommandArg(arg string) string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return `"` + strings.ReplaceAll(arg, `"`, `""`) + `"`
|
||||||
|
}
|
||||||
|
return `'` + strings.ReplaceAll(arg, `'`, `'\''`) + `'`
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinOutput(outputs ...string) string {
|
||||||
|
var parts []string
|
||||||
|
for _, output := range outputs {
|
||||||
|
output = strings.TrimRight(output, "\r\n")
|
||||||
|
if output != "" {
|
||||||
|
parts = append(parts, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAppModule(app, module string) AppModule {
|
||||||
|
path := filepath.Join(".", "config", app, module)
|
||||||
|
name := module
|
||||||
|
check := "SCRIPT"
|
||||||
|
configPath := filepath.Join(path, "config.yml")
|
||||||
|
if fileExists(configPath) {
|
||||||
|
configF, err := os.ReadFile(configPath)
|
||||||
|
if err == nil {
|
||||||
|
var cfg AppModuleConfig
|
||||||
|
if err := yaml.Unmarshal(configF, &cfg); err == nil {
|
||||||
|
if cfg.Name != "" {
|
||||||
|
name = cfg.Name
|
||||||
|
}
|
||||||
|
if cfg.CheckUrl != nil {
|
||||||
|
check = "URL " + *cfg.CheckUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppModule{Name: name, Folder: path, CheckS: check}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readApplication(folderName string) Application {
|
||||||
|
name := folderName
|
||||||
|
appFolder := filepath.Join(".", "config", folderName)
|
||||||
|
configPath := filepath.Join(appFolder, "config.yml")
|
||||||
|
if fileExists(configPath) {
|
||||||
|
configF, err := os.ReadFile(configPath)
|
||||||
|
if err == nil {
|
||||||
|
var cfg AppConfig
|
||||||
|
if err := yaml.Unmarshal(configF, &cfg); err == nil {
|
||||||
|
if cfg.Name != "" {
|
||||||
|
name = cfg.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(appFolder)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
var i []AppModule
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
i = append(i, readAppModule(folderName, e.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Application{Name: name, Folder: appFolder, AppModules: i}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listApplications() []Application {
|
||||||
|
entries, err := os.ReadDir("./config")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
var i []Application
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
i = append(i, readApplication(e.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch(url string, timeout time.Duration) (int, string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return resp.StatusCode, "", err
|
||||||
|
}
|
||||||
|
return resp.StatusCode, string(b), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user