mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-30 13:56:40 -04:00
Use go-boilerplate
This commit is contained in:
+1
-16
@@ -1,14 +1,9 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type ByteInputFetcher func(prompt string) []byte
|
||||
|
||||
var (
|
||||
PassphraseInputFunction ByteInputFetcher // Clients should set this to a function that fetches hidden input from the user
|
||||
Home, _ = os.UserHomeDir()
|
||||
GetPassphrase ByteInputFetcher // Clients should set this to a function that fetches hidden input from the user
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,20 +13,10 @@ const (
|
||||
FSPath = "\u259e" // ▞ Path separator
|
||||
FSMisc = "\u259f" // ▟ Misc. field separator (if \u259d is already used)
|
||||
|
||||
AnsiError = "\033[38;5;9m"
|
||||
AnsiReset = "\033[0m"
|
||||
|
||||
ErrorRead = 101
|
||||
ErrorWrite = 102
|
||||
ErrorSyncProcess = 103
|
||||
ErrorServerConnection = 104
|
||||
ErrorTargetNotFound = 105
|
||||
ErrorTargetExists = 106
|
||||
ErrorTargetWrongType = 107
|
||||
ErrorDecryption = 108
|
||||
ErrorEncryption = 109
|
||||
ErrorClipboard = 110
|
||||
ErrorOther = 111
|
||||
)
|
||||
|
||||
var GetPassphrase func() []byte
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
package core
|
||||
|
||||
import "github.com/rwinkhart/go-boilerplate/back"
|
||||
|
||||
var (
|
||||
EntryRoot = Home + "/.local/share/libmutton" // Path to libmutton entry directory
|
||||
ConfigDir = Home + "/.config/libmutton" // Path to libmutton configuration directory
|
||||
ConfigPath = ConfigDir + "/libmutton.ini" // Path to libmutton configuration file
|
||||
EntryRoot = back.Home + "/.local/share/libmutton" // Path to libmutton entry directory
|
||||
ConfigDir = back.Home + "/.config/libmutton" // Path to libmutton configuration directory
|
||||
ConfigPath = ConfigDir + "/libmutton.ini" // Path to libmutton configuration file
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,6 +5,8 @@ package core
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
)
|
||||
|
||||
// clipClearProcess clears the clipboard after 30 seconds if the clipboard contents have not changed.
|
||||
@@ -15,9 +17,9 @@ func clipClearProcess(assignedContents string) {
|
||||
clearClipboard := func() {
|
||||
err := cmdClear.Run()
|
||||
if err != nil {
|
||||
PrintError("Failed to clear clipboard", ErrorClipboard, true)
|
||||
back.PrintError("Failed to clear clipboard", ErrorClipboard, true)
|
||||
}
|
||||
Exit(0)
|
||||
back.Exit(0)
|
||||
}
|
||||
|
||||
// if assignedContents is empty, clear the clipboard immediately and unconditionally
|
||||
@@ -31,7 +33,7 @@ func clipClearProcess(assignedContents string) {
|
||||
|
||||
newContents, err := cmdPaste.Output()
|
||||
if err != nil {
|
||||
PrintError("Failed to read clipboard contents", ErrorClipboard, true)
|
||||
back.PrintError("Failed to read clipboard contents", ErrorClipboard, true)
|
||||
}
|
||||
|
||||
if assignedContents == strings.TrimRight(string(newContents), "\r\n") {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
func loadConfig() *ini.File {
|
||||
cfg, err := ini.Load(ConfigPath)
|
||||
if err != nil {
|
||||
PrintError("Failed to load libmutton.ini: "+err.Error(), ErrorRead, true)
|
||||
back.PrintError("Failed to load libmutton.ini: "+err.Error(), back.ErrorRead, true)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
@@ -38,11 +39,11 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]strin
|
||||
case "":
|
||||
err = fmt.Errorf("Failed to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0])
|
||||
case "0":
|
||||
Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
|
||||
back.Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
|
||||
default:
|
||||
err = fmt.Errorf("%s", missingValueError)
|
||||
}
|
||||
PrintError(err.Error(), ErrorRead, false)
|
||||
back.PrintError(err.Error(), back.ErrorRead, false)
|
||||
// if interactive (soft exit), return nil and the error to be handled by the caller
|
||||
return nil, err
|
||||
}
|
||||
@@ -60,7 +61,7 @@ func GenDeviceIDList(errorOnFail bool) *[]fs.DirEntry {
|
||||
deviceIDList, err := os.ReadDir(ConfigDir + PathSeparator + "devices")
|
||||
if err != nil {
|
||||
if errorOnFail {
|
||||
PrintError("Failed to read the devices directory: "+err.Error(), ErrorRead, true)
|
||||
back.PrintError("Failed to read the devices directory: "+err.Error(), back.ErrorRead, true)
|
||||
} else {
|
||||
return nil // a nil return value indicates that the devices directory could not be read/does not exist
|
||||
}
|
||||
@@ -109,6 +110,6 @@ func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool
|
||||
// save to libmutton.ini
|
||||
err := cfg.SaveTo(ConfigPath)
|
||||
if err != nil {
|
||||
PrintError("Failed to save libmutton.ini: "+err.Error(), ErrorWrite, true)
|
||||
back.PrintError("Failed to save libmutton.ini: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -8,11 +8,12 @@ import (
|
||||
|
||||
steamtotp "github.com/fortis/go-steam-totp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
)
|
||||
|
||||
// CopyArgument copies a field from an entry to the clipboard.
|
||||
func CopyArgument(targetLocation string, field int) {
|
||||
if isFile, _ := TargetIsFile(targetLocation, true, 2); isFile {
|
||||
if isFile, _ := back.TargetIsFile(targetLocation, true, 2); isFile {
|
||||
|
||||
decryptedEntry := DecryptFileToSlice(targetLocation)
|
||||
var copySubject string // will store data to be copied
|
||||
@@ -22,7 +23,7 @@ func CopyArgument(targetLocation string, field int) {
|
||||
|
||||
// ensure field is not empty
|
||||
if decryptedEntry[field] == "" {
|
||||
PrintError("Field is empty", ErrorTargetNotFound, true)
|
||||
back.PrintError("Field is empty", back.ErrorTargetNotFound, true)
|
||||
}
|
||||
|
||||
if field != 2 {
|
||||
@@ -48,7 +49,7 @@ func CopyArgument(targetLocation string, field int) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PrintError("Field does not exist in entry", ErrorTargetNotFound, true)
|
||||
back.PrintError("Field does not exist in entry", back.ErrorTargetNotFound, true)
|
||||
}
|
||||
|
||||
// copy field to clipboard, launch clipboard clearing process
|
||||
@@ -58,7 +59,7 @@ func CopyArgument(targetLocation string, field int) {
|
||||
|
||||
// ClipClearArgument reads the assigned clipboard contents from stdin and passes them to clipClearProcess.
|
||||
func ClipClearArgument() {
|
||||
assignedContents := readFromStdin()
|
||||
assignedContents := back.ReadFromStdin()
|
||||
if assignedContents == "" {
|
||||
os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment)
|
||||
}
|
||||
@@ -77,7 +78,7 @@ func GenTOTP(secret string, time time.Time, forSteam bool) string {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
PrintError("Error generating TOTP code", ErrorOther, true)
|
||||
back.PrintError("Error generating TOTP code", back.ErrorOther, true)
|
||||
}
|
||||
|
||||
return totpToken
|
||||
|
||||
+5
-3
@@ -5,6 +5,8 @@ package core
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
)
|
||||
|
||||
// copyString copies a string to the clipboard.
|
||||
@@ -18,13 +20,13 @@ func copyString(continuous bool, copySubject string) {
|
||||
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
|
||||
cmdCopy = exec.Command("xclip", "-sel", "c", "-t", "text/plain")
|
||||
} else {
|
||||
PrintError("Clipboard platform could not be determined", ErrorClipboard, true)
|
||||
back.PrintError("Clipboard platform could not be determined", ErrorClipboard, true)
|
||||
}
|
||||
|
||||
writeToStdin(cmdCopy, copySubject)
|
||||
back.WriteToStdin(cmdCopy, copySubject)
|
||||
err := cmdCopy.Run()
|
||||
if err != nil {
|
||||
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||
back.PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||
}
|
||||
|
||||
if !continuous {
|
||||
|
||||
+3
-1
@@ -1,9 +1,11 @@
|
||||
package core
|
||||
|
||||
import "github.com/rwinkhart/go-boilerplate/back"
|
||||
|
||||
// GetOldEntryData decrypts and returns old entry data (with all required lines present).
|
||||
func GetOldEntryData(targetLocation string, field int) []string {
|
||||
// ensure targetLocation exists
|
||||
TargetIsFile(targetLocation, true, 2)
|
||||
back.TargetIsFile(targetLocation, true, 2)
|
||||
|
||||
// read old entry data
|
||||
unencryptedEntry := DecryptFileToSlice(targetLocation)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
//go:build !interactive
|
||||
|
||||
package core
|
||||
|
||||
import "os"
|
||||
|
||||
// Exit (hard) is meant to be used in non-interactive CLI implementations to exit the program after an operation.
|
||||
func Exit(code int) {
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
//go:build interactive
|
||||
|
||||
package core
|
||||
|
||||
// Exit (soft) is meant to be used in interactive implementations (GUIs/TUIs) to keep the program running after an operation.
|
||||
func Exit(code int) int {
|
||||
return code
|
||||
}
|
||||
+6
-5
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/rcw/wrappers"
|
||||
)
|
||||
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
func RCWSanityCheckGen(passphrase []byte) {
|
||||
err := wrappers.GenSanityCheck(ConfigDir+PathSeparator+"sanity.rcw", passphrase)
|
||||
if err != nil {
|
||||
PrintError("Failed to generate sanity check file: "+err.Error(), ErrorWrite, true)
|
||||
back.PrintError("Failed to generate sanity check file: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +21,7 @@ func DirInit(preserveOldConfigDir bool) string {
|
||||
// create EntryRoot
|
||||
err := os.MkdirAll(EntryRoot, 0700)
|
||||
if err != nil {
|
||||
PrintError("Failed to create \""+EntryRoot+"\": "+err.Error(), ErrorWrite, true)
|
||||
back.PrintError("Failed to create \""+EntryRoot+"\": "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
|
||||
// get old device ID before its potential removal
|
||||
@@ -28,11 +29,11 @@ func DirInit(preserveOldConfigDir bool) string {
|
||||
|
||||
// remove existing config directory (if it exists and not in append mode)
|
||||
if !preserveOldConfigDir {
|
||||
_, isAccessible := TargetIsFile(ConfigDir, false, 1)
|
||||
_, isAccessible := back.TargetIsFile(ConfigDir, false, 1)
|
||||
if isAccessible {
|
||||
err = os.RemoveAll(ConfigDir)
|
||||
if err != nil {
|
||||
PrintError("Failed to remove existing config directory: "+err.Error(), ErrorWrite, true)
|
||||
back.PrintError("Failed to remove existing config directory: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +41,7 @@ func DirInit(preserveOldConfigDir bool) string {
|
||||
// create config directory w/devices subdirectory
|
||||
err = os.MkdirAll(ConfigDir+PathSeparator+"devices", 0700)
|
||||
if err != nil {
|
||||
PrintError("Failed to create \""+ConfigDir+"\": "+err.Error(), ErrorWrite, true)
|
||||
back.PrintError("Failed to create \""+ConfigDir+"\": "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
|
||||
return oldDeviceID
|
||||
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
)
|
||||
|
||||
// LaunchClipClearProcess launches the timed clipboard clearing process.
|
||||
// For non-interactive CLI implementations, an entirely separate process is created for this purpose.
|
||||
func LaunchClipClearProcess(copySubject string, isWayland bool) {
|
||||
cmd := exec.Command(os.Args[0], "clipclear", strconv.FormatBool(isWayland))
|
||||
writeToStdin(cmd, copySubject)
|
||||
back.WriteToStdin(cmd, copySubject)
|
||||
cmd.Start()
|
||||
os.Exit(0) // use os.Exit directly since this version of this function is only meant for non-interactive CLI implementations
|
||||
}
|
||||
|
||||
+7
-6
@@ -6,13 +6,14 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/rcw/daemon"
|
||||
"github.com/rwinkhart/rcw/wrappers"
|
||||
)
|
||||
|
||||
// RCWDArgument reads the passphrase from stdin and caches it via an RCW daemon.
|
||||
func RCWDArgument() {
|
||||
passphrase := readFromStdin()
|
||||
passphrase := back.ReadFromStdin()
|
||||
if passphrase == "" {
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -24,7 +25,7 @@ func DecryptFileToSlice(targetLocation string) []string {
|
||||
// read encrypted file
|
||||
encBytes, err := os.ReadFile(targetLocation)
|
||||
if err != nil {
|
||||
PrintError("Failed to open \""+targetLocation+"\" for decryption - "+err.Error(), ErrorDecryption, true)
|
||||
back.PrintError("Failed to open \""+targetLocation+"\" for decryption - "+err.Error(), back.ErrorRead, true)
|
||||
}
|
||||
|
||||
// decrypt data using RCW daemon
|
||||
@@ -37,7 +38,7 @@ func DecryptFileToSlice(targetLocation string) []string {
|
||||
// directly to avoid waiting for socket file creation
|
||||
decBytes, err := wrappers.Decrypt(encBytes, passphrase)
|
||||
if err != nil {
|
||||
PrintError("Failed to decrypt \""+targetLocation+"\" - "+err.Error(), ErrorDecryption, true)
|
||||
back.PrintError("Failed to decrypt \""+targetLocation+"\" - "+err.Error(), ErrorDecryption, true)
|
||||
}
|
||||
return strings.Split(string(decBytes), "\n")
|
||||
}
|
||||
@@ -62,15 +63,15 @@ func launchRCWDProcess() []byte {
|
||||
}
|
||||
var passphrase []byte
|
||||
for {
|
||||
passphrase = GetPassphrase()
|
||||
passphrase = GetPassphrase("RCW Passphrase:")
|
||||
err := wrappers.RunSanityCheck(ConfigDir+PathSeparator+"sanity.rcw", passphrase)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
fmt.Println(AnsiError + "Incorrect passphrase" + AnsiReset)
|
||||
fmt.Println(back.AnsiError + "Incorrect passphrase" + back.AnsiReset)
|
||||
}
|
||||
cmd := exec.Command(os.Args[0], "startrcwd")
|
||||
writeToStdin(cmd, string(passphrase))
|
||||
back.WriteToStdin(cmd, string(passphrase))
|
||||
cmd.Start()
|
||||
|
||||
return passphrase
|
||||
|
||||
+7
-93
@@ -1,93 +1,25 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TargetIsFile checks if the targetLocation is a file, directory, or is inaccessible.
|
||||
// Requires: failCondition (0 = fail on inaccessible, 1 = fail on inaccessible&file, 2 = fail on inaccessible&directory).
|
||||
// Returns: isFile, isAccessible.
|
||||
func TargetIsFile(targetLocation string, errorOnFail bool, failCondition uint8) (bool, bool) {
|
||||
targetInfo, err := os.Stat(targetLocation)
|
||||
if err != nil {
|
||||
if errorOnFail {
|
||||
PrintError("Failed to access \""+targetLocation+"\" - Ensure it exists and has the correct permissions", ErrorTargetNotFound, true)
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
if targetInfo.IsDir() {
|
||||
if errorOnFail && failCondition == 2 {
|
||||
PrintError("\""+targetLocation+"\" is a directory", ErrorTargetWrongType, true)
|
||||
}
|
||||
return false, true
|
||||
} else {
|
||||
if errorOnFail && failCondition == 1 {
|
||||
PrintError("\""+targetLocation+"\" is a file", ErrorTargetWrongType, true)
|
||||
}
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
)
|
||||
|
||||
// WriteEntry writes entryData to an encrypted file at targetLocation.
|
||||
func WriteEntry(targetLocation string, entryData []byte) {
|
||||
encryptedBytes := EncryptBytes(entryData)
|
||||
err := os.WriteFile(targetLocation, encryptedBytes, 0600)
|
||||
if err != nil {
|
||||
PrintError("Failed to write to file: "+err.Error(), ErrorWrite, true)
|
||||
back.PrintError("Failed to write to file: "+err.Error(), back.ErrorWrite, true)
|
||||
}
|
||||
}
|
||||
|
||||
// writeToStdin is a utility function that writes a string to a command's stdin.
|
||||
func writeToStdin(cmd *exec.Cmd, input string) {
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
PrintError("Failed to access stdin for system command: "+err.Error(), ErrorOther, true)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func(stdin io.WriteCloser) {
|
||||
_ = stdin.Close() // error ignored; if stdin could be accessed, it can probably be closed
|
||||
}(stdin)
|
||||
_, _ = io.WriteString(stdin, input)
|
||||
}()
|
||||
}
|
||||
|
||||
// readFromStdin is a utility function that reads a string from stdin.
|
||||
func readFromStdin() string {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
if scanner.Scan() {
|
||||
return scanner.Text()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CreateTempFile creates a temporary file and returns a pointer to it.
|
||||
func CreateTempFile() *os.File {
|
||||
tempFile, err := os.CreateTemp("", "*.markdown")
|
||||
if err != nil {
|
||||
PrintError("Failed to create temporary file: "+err.Error(), ErrorWrite, true)
|
||||
}
|
||||
return tempFile
|
||||
}
|
||||
|
||||
// RemoveTrailingEmptyStrings removes empty strings from the end of a slice.
|
||||
func RemoveTrailingEmptyStrings(slice []string) []string {
|
||||
for i := len(slice) - 1; i >= 0; i-- {
|
||||
if slice[i] != "" {
|
||||
return slice[:i+1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClampTrailingWhitespace strips trailing newlines, carriage returns, and tabs from each line in a note.
|
||||
// Additionally, it removes single trailing spaces and truncates multiple trailing spaces to two (for Markdown formatting).
|
||||
func ClampTrailingWhitespace(note []string) {
|
||||
@@ -123,16 +55,16 @@ func ClampTrailingWhitespace(note []string) {
|
||||
// Returns: statusCode (0 = success, 1 = target location already exists, 2 = containing directory is invalid).
|
||||
func EntryAddPrecheck(targetLocation string) uint8 {
|
||||
// ensure target location does not already exist
|
||||
_, isAccessible := TargetIsFile(targetLocation, false, 0)
|
||||
_, isAccessible := back.TargetIsFile(targetLocation, false, 0)
|
||||
if isAccessible {
|
||||
PrintError("Target location already exists", ErrorTargetExists, false)
|
||||
back.PrintError("Target location already exists", ErrorTargetExists, false)
|
||||
return 1 // inform interactive clients that the target location already exists
|
||||
}
|
||||
// ensure target containing directory exists and is a directory (not a file)
|
||||
containingDir := targetLocation[:strings.LastIndex(targetLocation, PathSeparator)]
|
||||
isFile, isAccisAccessible := TargetIsFile(containingDir, false, 1)
|
||||
isFile, isAccisAccessible := back.TargetIsFile(containingDir, false, 1)
|
||||
if isFile || !isAccisAccessible {
|
||||
PrintError("\""+containingDir+"\" is not a valid containing directory", ErrorTargetWrongType, false)
|
||||
back.PrintError("\""+containingDir+"\" is not a valid containing directory", back.ErrorTargetWrongType, false)
|
||||
return 2 // inform interactive clients that the containing directory is invalid
|
||||
}
|
||||
return 0
|
||||
@@ -205,21 +137,3 @@ func EntryIsNotEmpty(entryData []string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ExpandPathWithHome, given a path (as a string) containing "~", returns the path with "~" expanded to the user's home directory.
|
||||
func ExpandPathWithHome(path string) string {
|
||||
return strings.Replace(path, "~", Home, 1)
|
||||
}
|
||||
|
||||
// PrintError prints an error message in the standard libmutton format and exits with the specified exit code.
|
||||
// Requires: message (the error message to print),
|
||||
// exitCode (the exit code to use),
|
||||
// forceHardExit (if true, exit immediately; if false, allow soft exit for interactive clients).
|
||||
func PrintError(message string, exitCode int, forceHardExit bool) {
|
||||
fmt.Println(AnsiError + message + AnsiReset)
|
||||
if forceHardExit {
|
||||
os.Exit(exitCode)
|
||||
} else {
|
||||
Exit(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user