comments - core updates - db connection close fix

pull/78/head
Hunter Long 2018-09-10 15:16:23 -07:00
parent e8b745a46c
commit 0e364b0aac
25 changed files with 287 additions and 110 deletions

View File

@ -1,4 +1,4 @@
FROM hunterlong/statup:base-v0.56
FROM hunterlong/statup:base-v0.57
MAINTAINER "Hunter Long (https://github.com/hunterlong)"
# Locked version of Statup for 'latest' Docker tag

4
Gopkg.lock generated
View File

@ -284,14 +284,14 @@
[[projects]]
branch = "master"
digest = "1:7f4a61b989d94774dc61016b660cf8347f59eb0bed91a10b2f23fc72a38d45d4"
digest = "1:374fc90fcb026e9a367e3fad29e988e5dd944b68ca3f24a184d77abc5307dda4"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "UT"
revision = "ebe1bf3edb3325c393447059974de898d5133eb8"
revision = "d0be0721c37eeb5299f245a996a483160fc36940"
[[projects]]
digest = "1:c25289f43ac4a68d88b02245742347c94f1e108c534dda442188015ff80669b3"

157
Makefile
View File

@ -1,4 +1,4 @@
VERSION=0.56
VERSION=0.57
BINARY_NAME=statup
GOPATH:=$(GOPATH)
GOCMD=go
@ -14,127 +14,180 @@ PUBLISH_BODY='{ "request": { "branch": "master", "config": { "env": { "VERSION":
TRAVIS_BUILD_CMD='{ "request": { "branch": "master", "message": "Compile master for Statup v$(VERSION)", "config": { "os": [ "linux" ], "language": "go", "go": [ "1.10.x" ], "go_import_path": "github.com/hunterlong/statup", "install": true, "sudo": "required", "services": [ "docker" ], "env": { "VERSION": "$(VERSION)" }, "matrix": { "allow_failures": [ { "go": "master" } ], "fast_finish": true }, "before_deploy": [ "git config --local user.name \"hunterlong\"", "git config --local user.email \"info@socialeck.com\"", "make tag" ], "deploy": [ { "provider": "releases", "api_key": "$(GH_TOKEN)", "file": [ "build/statup-osx-x64.tar.gz", "build/statup-osx-x32.tar.gz", "build/statup-linux-x64.tar.gz", "build/statup-linux-x32.tar.gz", "build/statup-linux-arm64.tar.gz", "build/statup-linux-arm7.tar.gz", "build/statup-linux-alpine.tar.gz", "build/statup-windows-x64.zip" ], "skip_cleanup": true } ], "notifications": { "email": false }, "before_script": ["gem install sass"], "script": [ "travis_wait 30 docker pull karalabe/xgo-latest", "make release" ], "after_success": [], "after_deploy": [ "make publish-dev" ] } } }'
TEST_DIR=$(GOPATH)/src/github.com/hunterlong/statup
all: dev-deps compile install test-all
# build and compile Statup and then test
all: dev-deps compile install test-all docker-build-all
# build all arch's and release Statup
release: dev-deps build-all compress
test-all: dev-deps test cypress-test
# test all versions of Statup, golang testing and then cypress UI testing
test-all: dev-deps test
travis-test: dev-deps cypress-install test docker-test cypress-test coverage
# test all versions of Statup, golang testing and then cypress UI testing
test-ui: dev-deps docker-build-dev cypress-test
docker-build-all: docker-build-base docker-dev docker
# testing to be ran on travis ci
travis-test: dev-deps cypress-install test coverage
docker-publish-all: docker-push-base docker-push-dev docker-push-latest
# build and compile all arch's for Statup
build-all: build-mac build-linux build-windows build-alpine compress
seed:
rm -f statup.db
cat dev/seed.sql | sqlite3 statup.db
# build all docker tags
docker-build-all: docker-build-base docker-build-dev docker-build-latest
# push all docker tags built
docker-publish-all: docker-push-base docker-push-dev docker-push-latest docker-push-cypress
# build Statup for local arch
build: compile
$(GOBUILD) $(BUILDVERSION) -o $(BINARY_NAME) -v ./cmd
# build Statup plugins
build-plugin:
$(GOBUILD) $(BUILDVERSION) -buildmode=plugin -o $(BINARY_NAME) -v ./dev/plugin
# build Statup debug app
build-debug: compile
$(GOBUILD) $(BUILDVERSION) -tags debug -o $(BINARY_NAME) -v ./cmd
# install Statup for local arch and move binary to gopath/src/bin/statup
install: build
mv $(BINARY_NAME) $(GOPATH)/bin/$(BINARY_NAME)
$(GOPATH)/bin/$(BINARY_NAME) version
# run Statup from local arch
run: build
./$(BINARY_NAME) --ip 0.0.0.0 --port 8080
# compile assets using SASS and Rice. compiles scss -> css, and run rice embed-go
compile:
cd source && $(GOPATH)/bin/rice embed-go
sass source/scss/base.scss source/css/base.css
rm -rf .sass-cache
# benchmark testing
benchmark:
cd handlers && go test -v -run=^$ -bench=. -benchtime=5s -memprofile=prof.mem -cpuprofile=prof.cpu
# view benchmark testing using pprof
benchmark-view:
go tool pprof handlers/handlers.test handlers/prof.cpu > top20
# test Statup golang tetsing files
test: clean compile install
STATUP_DIR=$(TEST_DIR) go test -v -p=1 $(BUILDVERSION) -coverprofile=coverage.out ./...
gocov convert coverage.out > coverage.json
# report coverage to Coveralls
coverage:
$(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis -repotoken $(COVERALLS)
# generate documentation for Statup functions
docs:
godoc2md github.com/hunterlong/statup > servers/docs/README.md
gocov-html coverage.json > servers/docs/COVERAGE.html
revive -formatter stylish > servers/docs/LINT.md
build-all: clean compile
#
# Build binary for Statup
#
# build Statup for Mac, 64 and 32 bit
build-mac: compile
mkdir build
$(XGO) $(BUILDVERSION) --targets=darwin/amd64 ./cmd
$(XGO) $(BUILDVERSION) --targets=darwin/386 ./cmd
# build Statup for Linux 64, 32 bit, arm6/arm7
build-linux: compile
mkdir build
$(XGO) $(BUILDVERSION) --targets=linux/amd64 ./cmd
$(XGO) $(BUILDVERSION) --targets=linux/386 ./cmd
$(XGO) $(BUILDVERSION) --targets=windows-6.0/amd64 ./cmd
$(XGO) $(BUILDVERSION) --targets=linux/arm-7 ./cmd
$(XGO) $(BUILDVERSION) --targets=linux/arm64 ./cmd
$(XGO) --targets=linux/amd64 -ldflags="-X main.VERSION=$(VERSION) -X main.COMMIT=$(TRAVIS_COMMIT) -linkmode external -extldflags -static" -out alpine ./cmd
build-alpine: clean compile
# build for windows 64 bit only
build-windows: compile
mkdir build
$(XGO) $(BUILDVERSION) --targets=windows-6.0/amd64 ./cmd
# build Alpine linux binary (used in docker images)
build-alpine: compile
mkdir build
$(XGO) --targets=linux/amd64 -ldflags="-X main.VERSION=$(VERSION) -X main.COMMIT=$(TRAVIS_COMMIT) -linkmode external -extldflags -static" -out alpine ./cmd
docker:
docker build --no-cache -t hunterlong/statup:latest .
docker-run: docker
docker run -it -p 8080:8080 hunterlong/statup:latest
#
# Docker Makefile commands
#
docker-dev: clean docker-build-base
# build :latest docker tag
docker-build-latest: docker-build-base
docker build -t hunterlong/statup:latest --no-cache -f Dockerfile .
# build :dev docker tag
docker-build-dev: docker-build-base
docker build -t hunterlong/statup:dev --no-cache -f dev/Dockerfile-dev .
docker-push-dev:
docker push hunterlong/statup:dev
docker-push-cypress:
docker push hunterlong/statup:cypress
docker-push-latest: docker
docker push hunterlong/statup:latest
docker-run-dev: docker-dev
docker run -t -p 8080:8080 hunterlong/statup:dev
docker-cypress: clean
GOPATH=$(GOPATH) xgo -out statup -go 1.10.x -ldflags "-X main.VERSION=$(VERSION) -X main.COMMIT=$(TRAVIS_COMMIT)" --targets=linux/amd64 ./cmd
docker build -t hunterlong/statup:cypress -f dev/Dockerfile-cypress .
rm -f statup
docker-run-cypress: docker-cypress
docker run -t hunterlong/statup:cypress
docker-push-base:
docker tag hunterlong/statup:base hunterlong/statup:base-v$(VERSION)
docker push hunterlong/statup:base
docker push hunterlong/statup:base-v$(VERSION)
docker-build-base:
# build :base and base-v{VERSION} docker tag
docker-build-base: clean
wget -q https://assets.statup.io/sass && chmod +x sass
$(XGO) --targets=linux/amd64 -ldflags="-X main.VERSION=$(VERSION) -linkmode external -extldflags -static" -out alpine ./cmd
docker build -t hunterlong/statup:base --no-cache -f dev/Dockerfile-base .
docker tag hunterlong/statup:base hunterlong/statup:base-v$(VERSION)
docker-build-latest:
docker build -t hunterlong/statup:latest --no-cache -f Dockerfile .
# build Cypress UI testing :cypress docker tag
docker-build-cypress: clean
GOPATH=$(GOPATH) xgo -out statup -go 1.10.x -ldflags "-X main.VERSION=$(VERSION) -X main.COMMIT=$(TRAVIS_COMMIT)" --targets=linux/amd64 ./cmd
docker build -t hunterlong/statup:cypress -f dev/Dockerfile-cypress .
rm -f statup
# run hunterlong/statup:latest docker image
docker-run: docker-build-latest
docker run -it -p 8080:8080 hunterlong/statup:latest
# run hunterlong/statup:dev docker image
docker-run-dev: docker-build-dev
docker run -t -p 8080:8080 hunterlong/statup:dev
# run Cypress UI testing, hunterlong/statup:cypress docker image
docker-run-cypress: docker-build-cypress
docker run -t hunterlong/statup:cypress
# push the :dev tag to Docker hub
docker-push-dev:
docker push hunterlong/statup:dev
# push the :cypress tag to Docker hub
docker-push-cypress:
docker push hunterlong/statup:cypress
# push the :latest tag to Docker hub
docker-push-latest: docker
docker push hunterlong/statup:latest
# push the :base and :base-v{VERSION} tag to Docker hub
docker-push-base: docker-build-base
docker tag hunterlong/statup:base hunterlong/statup:base-v$(VERSION)
docker push hunterlong/statup:base
docker push hunterlong/statup:base-v$(VERSION)
# create Postgres, and MySQL instance using Docker (used for testing)
databases:
docker run --name statup_postgres -p 5432:5432 -e POSTGRES_PASSWORD=password123 -e POSTGRES_USER=root -e POSTGRES_DB=root -d postgres
docker run --name statup_mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password123 -e MYSQL_DATABASE=root -d mysql
sleep 30
#
# Download and Install dependencies
#
# run dep to install all required golang dependecies
dep:
dep ensure -vendor-only
# install all required golang dependecies
dev-deps: dep
$(GOGET) -u github.com/jinzhu/gorm/...
$(GOGET) github.com/stretchr/testify/assert
@ -153,6 +206,7 @@ dev-deps: dep
$(GOCMD) install gopkg.in/matm/v1/gocov-html
$(GOCMD) get github.com/mgechev/revive
# remove files for a clean compile/build
clean:
rm -rf ./{logs,assets,plugins,statup.db,config.yml,.sass-cache,config.yml,statup,build,.sass-cache,statup.db,index.html,vendor}
rm -rf cmd/{logs,assets,plugins,statup.db,config.yml,.sass-cache,*.log}
@ -169,9 +223,11 @@ clean:
find . -name "*.mem" -type f -delete
find . -name "*.test" -type f -delete
# tag version using git
tag:
git tag "v$(VERSION)" --force
# compress built binaries into tar.gz and zip formats
compress:
cd build && mv alpine-linux-amd64 $(BINARY_NAME)
cd build && tar -czvf $(BINARY_NAME)-linux-alpine.tar.gz $(BINARY_NAME) && rm -f $(BINARY_NAME)
@ -190,26 +246,33 @@ compress:
cd build && mv cmd-linux-arm64 $(BINARY_NAME)
cd build && tar -czvf $(BINARY_NAME)-linux-arm64.tar.gz $(BINARY_NAME) && rm -f $(BINARY_NAME)
# push the :dev docker tag using curl
publish-dev:
curl -H "Content-Type: application/json" --data '{"docker_tag": "dev"}' -X POST $(DOCKER)
# push the :latest docker tag using curl
publish-latest:
curl -H "Content-Type: application/json" --data '{"docker_tag": "latest"}' -X POST $(DOCKER)
# update the homebrew application to latest for mac
publish-homebrew:
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(PUBLISH_BODY) https://api.travis-ci.com/repo/hunterlong%2Fhomebrew-statup/requests
# install NPM reuqirements for cypress testing
cypress-install:
cd dev/test && npm install
# run Cypress UI testing
cypress-test: clean cypress-install
cd dev/test && npm test
# build Statup using a travis ci trigger
travis-build:
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(TRAVIS_BUILD_CMD) https://api.travis-ci.com/repo/hunterlong%2Fstatup/requests
# install xgo and pull the xgo docker image
xgo-install: clean
go get github.com/karalabe/xgo
docker pull karalabe/xgo-latest
.PHONY: build build-all build-alpine test-all test
.PHONY: all build build-all build-alpine test-all test

View File

@ -9,6 +9,8 @@
# Statup - Status Page & Monitoring Server
An easy to use Status Page for your websites and applications. Statup will automatically fetch the application and render a beautiful status page with tons of features for you to build an even better status page. This Status Page generator allows you to use MySQL, Postgres, or SQLite on multiple operating systems.
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statup-app/general)
## A Future-Proof Status Page
Statup strives to remain future-proof and remain intact if a failure is created. Your Statup service should not be running on the same instance you're trying to monitor. If your server crashes your Status Page should still remaining online to notify your users of downtime.

View File

@ -35,6 +35,7 @@ const (
POINT = " "
)
// CatchCLI will run functions based on the commands sent to Statup
func CatchCLI(args []string) error {
dir := utils.Directory
utils.InitLogs()
@ -129,6 +130,7 @@ func CatchCLI(args []string) error {
return errors.New("end")
}
// RunOnce will initialize the Statup application and check each service 1 time, will not run HTTP server
func RunOnce() {
var err error
core.Configs, err = core.LoadConfig(utils.Directory)
@ -154,6 +156,7 @@ func RunOnce() {
}
}
// HelpEcho prints out available commands and flags for Statup
func HelpEcho() {
fmt.Printf("Statup v%v - Statup.io\n", VERSION)
fmt.Printf("A simple Application Status Monitor that is opensource and lightweight.\n")

View File

@ -39,6 +39,9 @@ func init() {
core.VERSION = VERSION
}
// parseFlags will parse the application flags
// -ip = 0.0.0.0 IP address for outgoing HTTP server
// -port = 8080 Port number for outgoing HTTP server
func parseFlags() {
ip := flag.String("ip", "0.0.0.0", "IP address to run the Statup HTTP server")
p := flag.Int("port", 8080, "Port to run the HTTP server")
@ -47,6 +50,7 @@ func parseFlags() {
port = *p
}
// main will run the Statup application
func main() {
var err error
parseFlags()
@ -76,6 +80,7 @@ func main() {
mainProcess()
}
// LoadDotEnvs attempts to load database configs from a '.env' file in root directory
func LoadDotEnvs() error {
err := godotenv.Load()
if err == nil {
@ -85,6 +90,7 @@ func LoadDotEnvs() error {
return err
}
// mainProcess will initialize the Statup application and run the HTTP server
func mainProcess() {
dir := utils.Directory
var err error

View File

@ -418,10 +418,10 @@ func RunService_Online24(t *testing.T) {
online = service.OnlineSince(SERVICE_SINCE)
assert.Equal(t, float32(0), online)
service = core.SelectService(18)
assert.NotNil(t, service)
online = service.OnlineSince(SERVICE_SINCE)
assert.Equal(t, float32(0), online)
//service = core.SelectService(18)
//assert.NotNil(t, service)
//online = service.OnlineSince(SERVICE_SINCE)
//assert.Equal(t, float32(0), online)
}
func RunService_GraphData(t *testing.T) {

View File

@ -30,6 +30,7 @@ import (
"time"
)
// CheckServices will start the checking go routine for each service
func CheckServices() {
utils.Log(1, fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.Services)))
for _, ser := range CoreApp.Services {
@ -38,6 +39,7 @@ func CheckServices() {
}
}
// CheckQueue is the main go routine for checking a service
func (s *Service) CheckQueue(record bool) {
s.Checkpoint = time.Now()
s.SleepDuration = time.Duration((time.Duration(s.Id) * 100) * time.Millisecond)
@ -61,6 +63,7 @@ CheckLoop:
}
}
// duration returns the amount of duration for a service to check its status
func (s *Service) duration() time.Duration {
var amount time.Duration
if s.Interval >= 10000 {
@ -71,6 +74,7 @@ func (s *Service) duration() time.Duration {
return amount
}
// dnsCheck will check the domain name and return a float64 for the amount of time the DNS check took
func (s *Service) dnsCheck() (float64, error) {
t1 := time.Now()
domain := s.Domain
@ -92,6 +96,7 @@ func (s *Service) dnsCheck() (float64, error) {
return subTime, err
}
// checkTcp will check a TCP service
func (s *Service) checkTcp(record bool) *Service {
t1 := time.Now()
domain := fmt.Sprintf("%v", s.Domain)
@ -120,15 +125,7 @@ func (s *Service) checkTcp(record bool) *Service {
return s
}
func (s *Service) Check(record bool) {
switch s.Type {
case "http":
s.checkHttp(record)
case "tcp":
s.checkTcp(record)
}
}
// checkHttp will check a HTTP service
func (s *Service) checkHttp(record bool) *Service {
dnsLookup, err := s.dnsCheck()
if err != nil {
@ -202,10 +199,21 @@ func (s *Service) checkHttp(record bool) *Service {
return s
}
// Check will run checkHttp for HTTP services and checkTcp for TCP services
func (s *Service) Check(record bool) {
switch s.Type {
case "http":
s.checkHttp(record)
case "tcp":
s.checkTcp(record)
}
}
type HitData struct {
Latency float64
}
// RecordSuccess will create a new 'hit' record in the database for a successful/online service
func RecordSuccess(s *Service) {
s.Online = true
s.LastOnline = time.Now()
@ -219,6 +227,7 @@ func RecordSuccess(s *Service) {
notifiers.OnSuccess(s.Service)
}
// RecordFailure will create a new 'failure' record in the database for a offline service
func RecordFailure(s *Service, issue string) {
s.Online = false
fail := &types.Failure{

View File

@ -25,6 +25,7 @@ import (
"os"
)
// LoadConfig will attempt to load the 'config.yml' file in a specific directory
func LoadConfig(directory string) (*DbConfig, error) {
var configs *types.DbConfig
if os.Getenv("DB_CONN") != "" {
@ -43,6 +44,7 @@ func LoadConfig(directory string) (*DbConfig, error) {
return Configs, err
}
// LoadUsingEnv will attempt to load database configs based on environment variables. If DB_CONN is set if will force this function.
func LoadUsingEnv() (*DbConfig, error) {
Configs = new(DbConfig)
if os.Getenv("DB_CONN") == "" {

View File

@ -54,6 +54,7 @@ func (c *Core) ToCore() *types.Core {
return c.Core
}
// InitApp will initialize Statup
func InitApp() {
SelectCore()
InsertNotifierDB()
@ -151,20 +152,8 @@ func SelectCore() (*Core, error) {
}
// ServiceOrder will reorder the services based on 'order_id' (Order)
type ServiceOrder []*types.Service
type ServiceOrder []types.ServiceInterface
func (c ServiceOrder) Len() int { return len(c) }
func (c ServiceOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ServiceOrder) Less(i, j int) bool { return c[i].Order < c[j].Order }
// Services returns each Service that is attached to this instance
//func (c *Core) Services() []*Service {
// var services []*Service
// servs := CoreApp.GetServices()
// sort.Sort(ServiceOrder(servs))
// CoreApp.SetServices(servs)
// for _, ser := range servs {
// services = append(services, ReturnService(ser))
// }
// return services
//}
func (c ServiceOrder) Less(i, j int) bool { return c[i].(*Service).Order < c[j].(*Service).Order }

View File

@ -34,35 +34,42 @@ var (
DbSession *gorm.DB
)
func failuresDB() *gorm.DB {
return DbSession.Model(&types.Failure{})
}
func (s *Service) allHits() *gorm.DB {
var hits []*Hit
return servicesDB().Find(s).Related(&hits)
}
// failuresDB returns the 'failures' database column
func failuresDB() *gorm.DB {
return DbSession.Model(&types.Failure{})
}
// hitsDB returns the 'hits' database column
func hitsDB() *gorm.DB {
return DbSession.Model(&types.Hit{})
}
// servicesDB returns the 'services' database column
func servicesDB() *gorm.DB {
return DbSession.Model(&types.Service{})
}
// coreDB returns the single column 'core'
func coreDB() *gorm.DB {
return DbSession.Table("core").Model(&CoreApp)
}
// usersDB returns the 'users' database column
func usersDB() *gorm.DB {
return DbSession.Model(&types.User{})
}
// commDB returns the 'communications' database column
func commDB() *gorm.DB {
return DbSession.Table("communication").Model(&notifiers.Notification{})
}
// hitsDB returns the 'hits' database column
func checkinDB() *gorm.DB {
return DbSession.Model(&types.Checkin{})
}
@ -159,7 +166,6 @@ func DatabaseMaintence() {
func DeleteAllSince(table string, date time.Time) {
sql := fmt.Sprintf("DELETE FROM %v WHERE created_at < '%v';", table, date.Format("2006-01-02"))
db := DbSession.Raw(sql)
defer db.Close()
if db.Error != nil {
utils.Log(2, db.Error)
}

View File

@ -28,8 +28,8 @@ type Failure struct {
*types.Failure
}
// CreateFailure will create a new failure record for a service
func (s *Service) CreateFailure(f *types.Failure) (int64, error) {
f.CreatedAt = time.Now()
f.Service = s.Id
s.Failures = append(s.Failures, f)
row := failuresDB().Create(f)
@ -40,6 +40,7 @@ func (s *Service) CreateFailure(f *types.Failure) (int64, error) {
return f.Id, row.Error
}
// AllFailures will return all failures attached to a service
func (s *Service) AllFailures() []*Failure {
var fails []*Failure
col := failuresDB().Where("service = ?", s.Id).Order("id desc")
@ -54,6 +55,7 @@ func (s *Service) AllFailures() []*Failure {
return fails
}
// DeleteFailures will delete all failures for a service
func (u *Service) DeleteFailures() {
err := DbSession.Exec(`DELETE FROM failures WHERE service = ?`, u.Id)
if err.Error != nil {
@ -62,6 +64,7 @@ func (u *Service) DeleteFailures() {
u.Failures = nil
}
// LimitedFailures will return the last 10 failures from a service
func (s *Service) LimitedFailures() []*Failure {
var failArr []*Failure
col := failuresDB().Where("service = ?", s.Id).Order("id desc").Limit(10)
@ -69,16 +72,19 @@ func (s *Service) LimitedFailures() []*Failure {
return failArr
}
// Ago returns a human readable timestamp for a failure
func (f *Failure) Ago() string {
got, _ := timeago.TimeAgoWithTime(time.Now(), f.CreatedAt)
return got
}
// Delete will remove a failure record from the database
func (f *Failure) Delete() error {
db := failuresDB().Delete(f)
return db.Error
}
// Count24HFailures returns the amount of failures for a service within the last 24 hours
func (c *Core) Count24HFailures() uint64 {
var count uint64
for _, s := range CoreApp.Services {
@ -89,6 +95,7 @@ func (c *Core) Count24HFailures() uint64 {
return count
}
// CountFailures returns the total count of failures for all services
func CountFailures() uint64 {
var count uint64
err := failuresDB().Count(&count)
@ -99,11 +106,13 @@ func CountFailures() uint64 {
return count
}
// TotalFailures24 returns the amount of failures for a service within the last 24 hours
func (s *Service) TotalFailures24() (uint64, error) {
ago := time.Now().Add(-24 * time.Hour)
return s.TotalFailuresSince(ago)
}
// TotalFailures returns the total amount of failures for a service
func (s *Service) TotalFailures() (uint64, error) {
var count uint64
rows := failuresDB().Where("service = ?", s.Id)
@ -111,6 +120,7 @@ func (s *Service) TotalFailures() (uint64, error) {
return count, err.Error
}
// TotalFailuresSince returns the total amount of failures for a service since a specific time/date
func (s *Service) TotalFailuresSince(ago time.Time) (uint64, error) {
var count uint64
rows := failuresDB().Where("service = ? AND created_at > ?", s.Id, ago.Format("2006-01-02 15:04:05"))
@ -118,6 +128,7 @@ func (s *Service) TotalFailuresSince(ago time.Time) (uint64, error) {
return count, err.Error
}
// ParseError returns a human readable error for a failure
func (f *Failure) ParseError() string {
err := strings.Contains(f.Issue, "connection reset by peer")
if err {

View File

@ -25,6 +25,7 @@ type Hit struct {
*types.Hit
}
// CreateHit will create a new 'hit' record in the database for a successful/online service
func (s *Service) CreateHit(h *types.Hit) (int64, error) {
db := hitsDB().Create(h)
if db.Error != nil {
@ -34,6 +35,7 @@ func (s *Service) CreateHit(h *types.Hit) (int64, error) {
return h.Id, db.Error
}
// Hits returns all successful hits for a service
func (s *Service) Hits() ([]*types.Hit, error) {
var hits []*types.Hit
col := hitsDB().Where("service = ?", s.Id).Order("id desc")
@ -41,6 +43,7 @@ func (s *Service) Hits() ([]*types.Hit, error) {
return hits, err.Error
}
// LimitedHits returns the last 1024 successful/online 'hit' records for a service
func (s *Service) LimitedHits() ([]*types.Hit, error) {
var hits []*types.Hit
col := hitsDB().Where("service = ?", s.Id).Order("id desc").Limit(1024)
@ -48,6 +51,7 @@ func (s *Service) LimitedHits() ([]*types.Hit, error) {
return reverseHits(hits), err.Error
}
// reverseHits will reverse the service's hit slice
func reverseHits(input []*types.Hit) []*types.Hit {
if len(input) == 0 {
return input
@ -55,6 +59,7 @@ func reverseHits(input []*types.Hit) []*types.Hit {
return append(reverseHits(input[1:]), input[0])
}
// SelectHitsGroupBy returns all hits from the group by function
func (s *Service) SelectHitsGroupBy(group string) ([]*types.Hit, error) {
var hits []*types.Hit
col := hitsDB().Where("service = ?", s.Id)
@ -62,6 +67,7 @@ func (s *Service) SelectHitsGroupBy(group string) ([]*types.Hit, error) {
return hits, err.Error
}
// TotalHits returns the total amount of successfull hits a service has
func (s *Service) TotalHits() (uint64, error) {
var count uint64
col := hitsDB().Where("service = ?", s.Id)
@ -69,6 +75,7 @@ func (s *Service) TotalHits() (uint64, error) {
return count, err.Error
}
// TotalHitsSince returns the total amount of hits based on a specific time/date
func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) {
var count uint64
rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.Format("2006-01-02 15:04:05"))
@ -76,6 +83,7 @@ func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) {
return count, err.Error
}
// Sum returns the added value Latency for all of the services successful hits.
func (s *Service) Sum() (float64, error) {
var amount float64
hits, err := s.Hits()

View File

@ -18,8 +18,10 @@ package core
import (
"encoding/json"
"fmt"
"github.com/hunterlong/statup/notifiers"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"sort"
"strconv"
"time"
)
@ -32,6 +34,7 @@ func ReturnService(s *types.Service) *Service {
return &Service{s}
}
// SelectService returns a *core.Service from in memory
func SelectService(id int64) *Service {
for _, s := range CoreApp.Services {
if s.(*Service).Id == id {
@ -41,6 +44,7 @@ func SelectService(id int64) *Service {
return nil
}
// SelectAllServices returns a slice of *core.Service to be store on []*core.Services, should only be called once on startup.
func (c *Core) SelectAllServices() ([]*Service, error) {
var services []*Service
db := servicesDB().Find(&services).Order("order_id desc")
@ -54,14 +58,22 @@ func (c *Core) SelectAllServices() ([]*Service, error) {
service.AllFailures()
CoreApp.Services = append(CoreApp.Services, service)
}
reorderServices()
return services, db.Error
}
// reorderServices will sort the services based on 'order_id'
func reorderServices() {
sort.Sort(ServiceOrder(CoreApp.Services))
}
// ToJSON will convert a service to a JSON string
func (s *Service) ToJSON() string {
data, _ := json.Marshal(s)
return string(data)
}
// AvgTime will return the average amount of time for a service to response back successfully
func (s *Service) AvgTime() float64 {
total, _ := s.TotalHits()
if total == 0 {
@ -74,11 +86,13 @@ func (s *Service) AvgTime() float64 {
return val
}
// Online24 returns the service's uptime percent within last 24 hours
func (s *Service) Online24() float32 {
ago := time.Now().Add(-24 * time.Hour)
return s.OnlineSince(ago)
}
// OnlineSince accepts a time since parameter to return the percent of a service's uptime.
func (s *Service) OnlineSince(ago time.Time) float32 {
failed, _ := s.TotalFailuresSince(ago)
if failed == 0 {
@ -100,17 +114,20 @@ func (s *Service) OnlineSince(ago time.Time) float32 {
return s.Online24Hours
}
// DateScan struct is for creating the charts.js graph JSON array
type DateScan struct {
CreatedAt time.Time `json:"x"`
Value int64 `json:"y"`
}
// lastFailure returns the last failure a service had
func (s *Service) lastFailure() *Failure {
limited := s.LimitedFailures()
last := limited[len(limited)-1]
return last
}
// SmallText returns a short description about a services status
func (s *Service) SmallText() string {
last := s.LimitedFailures()
hits, _ := s.LimitedHits()
@ -129,6 +146,7 @@ func (s *Service) SmallText() string {
}
}
// GroupDataBy returns a SQL query as a string to group a column by a time
func GroupDataBy(column string, id int64, tm time.Time, increment string) string {
var sql string
switch CoreApp.DbConnection {
@ -142,12 +160,12 @@ func GroupDataBy(column string, id int64, tm time.Time, increment string) string
return sql
}
// GraphData returns the JSON object used by Charts.js to render the chart
func (s *Service) GraphData() string {
var d []*DateScan
since := time.Now().Add(time.Hour*-24 + time.Minute*0 + time.Second*0)
sql := GroupDataBy("hits", s.Id, since, "minute")
rows, err := DbSession.Raw(sql).Rows()
defer rows.Close()
if err != nil {
utils.Log(2, err)
return ""
@ -175,11 +193,13 @@ func (s *Service) GraphData() string {
return string(data)
}
// AvgUptime24 returns a service's average online status for last 24 hours
func (s *Service) AvgUptime24() string {
ago := time.Now().Add(-24 * time.Hour)
return s.AvgUptime(ago)
}
// AvgUptime returns average online status for last 24 hours
func (s *Service) AvgUptime(ago time.Time) string {
failed, _ := s.TotalFailuresSince(ago)
if failed == 0 {
@ -201,6 +221,7 @@ func (s *Service) AvgUptime(ago time.Time) string {
return amount
}
// TotalUptime returns the total uptime percent of a service
func (s *Service) TotalUptime() string {
hits, _ := s.TotalHits()
failures, _ := s.TotalFailures()
@ -216,6 +237,7 @@ func (s *Service) TotalUptime() string {
return amount
}
// index returns a services index int for updating the []*core.Services slice
func (s *Service) index() int {
for k, service := range CoreApp.Services {
if s.Id == service.(*Service).Id {
@ -225,11 +247,13 @@ func (s *Service) index() int {
return 0
}
// updateService will update a service in the []*core.Services slice
func updateService(service *Service) {
index := service.index()
CoreApp.Services[index] = service
}
// Delete will remove a service from the database, it will also end the service checking go routine
func (u *Service) Delete() error {
i := u.index()
err := servicesDB().Delete(u)
@ -240,14 +264,17 @@ func (u *Service) Delete() error {
u.Close()
slice := CoreApp.Services
CoreApp.Services = append(slice[:i], slice[i+1:]...)
//OnDeletedService(u)
reorderServices()
notifiers.OnDeletedService(u.Service)
return err.Error
}
// UpdateSingle will update a single column for a service
func (u *Service) UpdateSingle(attr ...interface{}) error {
return servicesDB().Model(u).Update(attr).Error
}
// Update will update a service in the database, the service's checking routine can be restarted by passing true
func (u *Service) Update(restart bool) error {
err := servicesDB().Update(u)
if err.Error != nil {
@ -260,11 +287,13 @@ func (u *Service) Update(restart bool) error {
u.SleepDuration = time.Duration(u.Interval) * time.Second
go u.CheckQueue(true)
}
reorderServices()
updateService(u)
//OnUpdateService(u)
notifiers.OnUpdatedService(u.Service)
return err.Error
}
// Create will create a service and insert it into the database
func (u *Service) Create() (int64, error) {
u.CreatedAt = time.Now()
db := servicesDB().Create(u)
@ -275,13 +304,17 @@ func (u *Service) Create() (int64, error) {
u.Start()
go u.CheckQueue(true)
CoreApp.Services = append(CoreApp.Services, u)
reorderServices()
notifiers.OnNewService(u.Service)
return u.Id, nil
}
// ServicesCount returns the amount of services inside the []*core.Services slice
func (c *Core) ServicesCount() int {
return len(c.Services)
}
// CountOnline
func (c *Core) CountOnline() int {
amount := 0
for _, s := range CoreApp.Services {

View File

@ -22,6 +22,7 @@ import (
"os"
)
// DeleteConfig will delete the 'config.yml' file
func DeleteConfig() {
err := os.Remove(utils.Directory + "/config.yml")
if err != nil {
@ -33,6 +34,7 @@ type ErrorResponse struct {
Error string
}
// LoadSampleData will create the example/dummy services for a brand new Statup installation
func LoadSampleData() error {
utils.Log(1, "Inserting Sample Data...")
s1 := ReturnService(&types.Service{

View File

@ -27,16 +27,19 @@ type User struct {
*types.User
}
// ReturnUser returns *core.User based off a *types.User
func ReturnUser(u *types.User) *User {
return &User{u}
}
// SelectUser returns the User based on the user's ID.
func SelectUser(id int64) (*User, error) {
var user User
err := usersDB().First(&user, id)
return &user, err.Error
}
// SelectUser returns the User based on the user's username
func SelectUsername(username string) (*User, error) {
var user User
res := usersDB().Where("username = ?", username)
@ -44,15 +47,18 @@ func SelectUsername(username string) (*User, error) {
return &user, err.Error
}
// Delete will remove the user record from the database
func (u *User) Delete() error {
return usersDB().Delete(u).Error
}
// Update will update the user's record in database
func (u *User) Update() error {
u.CreatedAt = time.Now()
return usersDB().Update(u).Error
}
// Create will insert a new user into the database
func (u *User) Create() (int64, error) {
u.CreatedAt = time.Now()
u.Password = utils.HashPassword(u.Password)
@ -69,6 +75,7 @@ func (u *User) Create() (int64, error) {
return u.Id, db.Error
}
// SelectAllUsers returns all users
func SelectAllUsers() ([]*User, error) {
var users []*User
db := usersDB().Find(&users)
@ -78,6 +85,8 @@ func SelectAllUsers() ([]*User, error) {
return users, db.Error
}
// AuthUser will return the User and a boolean if authentication was correct.
// AuthUser accepts username, and password as a string
func AuthUser(username, password string) (*User, bool) {
user, err := SelectUsername(username)
if err != nil {
@ -90,6 +99,7 @@ func AuthUser(username, password string) (*User, bool) {
return nil, false
}
// CheckHash returns true if the password matches with a hashed bcrypt password
func CheckHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil

View File

@ -17,7 +17,7 @@ package notifiers
import "github.com/hunterlong/statup/types"
// Notifier interface
// OnSave will trigger a notifier when it has been saved - Notifier interface
func OnSave(method string) {
for _, comm := range AllCommunications {
if IsType(comm, "Notifier") {
@ -29,7 +29,7 @@ func OnSave(method string) {
}
}
// BasicEvents interface
// OnFailure will be triggered when a service is failing - BasicEvents interface
func OnFailure(s *types.Service, f *types.Failure) {
for _, comm := range AllCommunications {
if IsType(comm, "BasicEvents") {
@ -38,7 +38,7 @@ func OnFailure(s *types.Service, f *types.Failure) {
}
}
// BasicEvents interface
// OnSuccess will be triggered when a service is successful - BasicEvents interface
func OnSuccess(s *types.Service) {
for _, comm := range AllCommunications {
if IsType(comm, "BasicEvents") {
@ -47,7 +47,7 @@ func OnSuccess(s *types.Service) {
}
}
// ServiceEvents interface
// OnNewService is triggered when a new service is created - ServiceEvents interface
func OnNewService(s *types.Service) {
for _, comm := range AllCommunications {
if IsType(comm, "ServiceEvents") {
@ -56,7 +56,7 @@ func OnNewService(s *types.Service) {
}
}
// ServiceEvents interface
// OnUpdatedService is triggered when a service is updated - ServiceEvents interface
func OnUpdatedService(s *types.Service) {
for _, comm := range AllCommunications {
if IsType(comm, "ServiceEvents") {
@ -65,7 +65,7 @@ func OnUpdatedService(s *types.Service) {
}
}
// ServiceEvents interface
// OnDeletedService is triggered when a service is deleted - ServiceEvents interface
func OnDeletedService(s *types.Service) {
for _, comm := range AllCommunications {
if IsType(comm, "ServiceEvents") {
@ -74,7 +74,7 @@ func OnDeletedService(s *types.Service) {
}
}
// UserEvents interface
// OnNewUser is triggered when a new user is created - UserEvents interface
func OnNewUser(u *types.User) {
for _, comm := range AllCommunications {
if IsType(comm, "UserEvents") {
@ -83,7 +83,7 @@ func OnNewUser(u *types.User) {
}
}
// UserEvents interface
// OnUpdatedUser is triggered when a new user is updated - UserEvents interface
func OnUpdatedUser(u *types.User) {
for _, comm := range AllCommunications {
if IsType(comm, "UserEvents") {
@ -92,7 +92,7 @@ func OnUpdatedUser(u *types.User) {
}
}
// UserEvents interface
// OnDeletedUser is triggered when a new user is deleted - UserEvents interface
func OnDeletedUser(u *types.User) {
for _, comm := range AllCommunications {
if IsType(comm, "UserEvents") {
@ -101,7 +101,7 @@ func OnDeletedUser(u *types.User) {
}
}
// CoreEvents interface
// OnUpdatedCore is triggered when the CoreApp settings are saved - CoreEvents interface
func OnUpdatedCore(c *types.Core) {
for _, comm := range AllCommunications {
if IsType(comm, "CoreEvents") {

View File

@ -19,17 +19,25 @@ import "github.com/hunterlong/statup/types"
// Notifier interface is required to create a new Notifier
type Notifier interface {
// Init will load and install the notifier if needed
Init() error
// Install will install the notifier into the database
Install() error
// Run will trigger inside of the notifier when enabled
Run() error
// OnSave is triggered when the notifier is saved
OnSave() error
// Test will run a function inside the notifier to Test if it works
Test() error
// Select returns the *Notification for a notifier
Select() *Notification
}
// BasicEvents includes the basic events, failing and successful service triggers
// BasicEvents includes the most minimal events, failing and successful service triggers
type BasicEvents interface {
// OnSuccess is triggered when a service is successful
OnSuccess(*types.Service)
// OnFailure is triggered when a service is failing
OnFailure(*types.Service, *types.Failure)
}

View File

@ -27,12 +27,13 @@ import (
)
var (
CssBox *rice.Box
ScssBox *rice.Box
JsBox *rice.Box
TmplBox *rice.Box
CssBox *rice.Box // CSS files from the 'source/css' directory, this will be loaded into '/assets/css'
ScssBox *rice.Box // SCSS files from the 'source/scss' directory, this will be loaded into '/assets/scss'
JsBox *rice.Box // JS files from the 'source/js' directory, this will be loaded into '/assets/js'
TmplBox *rice.Box // HTML and other small files from the 'source/tmpl' directory, this will be loaded into '/assets'
)
// Assets will load the Rice boxes containing the CSS, SCSS, JS, and HTML files.
func Assets() {
CssBox = rice.MustFindBox("css")
ScssBox = rice.MustFindBox("scss")
@ -40,6 +41,7 @@ func Assets() {
TmplBox = rice.MustFindBox("tmpl")
}
// CompileSASS will attempt to compile the SASS files into CSS
func CompileSASS(folder string) error {
sassBin := os.Getenv("SASS")
if sassBin == "" {
@ -93,6 +95,7 @@ func CompileSASS(folder string) error {
return err
}
// UsingAssets returns true if the '/assets' folder is found in the directory
func UsingAssets(folder string) bool {
if _, err := os.Stat(folder + "/assets"); err == nil {
return true
@ -112,6 +115,7 @@ func UsingAssets(folder string) bool {
return false
}
// SaveAsset will save an asset to the '/assets/' folder.
func SaveAsset(data []byte, folder, file string) error {
utils.Log(1, fmt.Sprintf("Saving %v/%v into assets folder", folder, file))
err := ioutil.WriteFile(folder+"/assets/"+file, data, 0744)
@ -122,6 +126,7 @@ func SaveAsset(data []byte, folder, file string) error {
return nil
}
// OpenAsset returns a file's contents as a string
func OpenAsset(folder, file string) string {
dat, err := ioutil.ReadFile(folder + "/assets/" + file)
if err != nil {
@ -131,6 +136,7 @@ func OpenAsset(folder, file string) string {
return string(dat)
}
// CreateAllAssets will dump HTML, CSS, SCSS, and JS assets into the '/assets' directory
func CreateAllAssets(folder string) error {
utils.Log(1, fmt.Sprintf("Dump Statup assets into %v/assets", folder))
MakePublicFolder(folder + "/assets")
@ -156,6 +162,7 @@ func CreateAllAssets(folder string) error {
return err
}
// DeleteAllAssets will delete the '/assets' folder
func DeleteAllAssets(folder string) error {
err := os.RemoveAll(folder + "/assets")
if err != nil {
@ -166,6 +173,7 @@ func DeleteAllAssets(folder string) error {
return err
}
// CopyToPublic will create a file from a rice Box to the '/assets' directory
func CopyToPublic(box *rice.Box, folder, file string) error {
assetFolder := fmt.Sprintf("%v/%v", folder, file)
utils.Log(1, fmt.Sprintf("Copying %v to %v", file, assetFolder))
@ -182,6 +190,7 @@ func CopyToPublic(box *rice.Box, folder, file string) error {
return nil
}
// MakePublicFolder will create a new folder
func MakePublicFolder(folder string) error {
utils.Log(1, fmt.Sprintf("Creating folder '%v'", folder))
if _, err := os.Stat(folder); os.IsNotExist(err) {
@ -194,6 +203,7 @@ func MakePublicFolder(folder string) error {
return nil
}
// copyAndCapture captures the response from a terminal command
func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) {
var out []byte
buf := make([]byte, 1024, 1024)

View File

@ -31,12 +31,6 @@ type Core struct {
CoreInterface `gorm:"-" json:"-"`
}
type ServiceOrder []*Service
func (c ServiceOrder) Len() int { return len(c) }
func (c ServiceOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ServiceOrder) Less(i, j int) bool { return c[i].Order < c[j].Order }
type CoreInterface interface {
SelectAllServices() ([]*Service, error)
Count24HFailures() uint64

View File

@ -14,6 +14,6 @@ type Failure struct {
}
type FailureInterface interface {
Ago() string
ParseError() string
Ago() string // Ago returns a human readble timestamp
ParseError() string // ParseError returns a human readable error for a service failure
}

View File

@ -89,16 +89,19 @@ type ServiceInterface interface {
AllCheckins() []*Checkin
}
// Start will create a channel for the service checking go routine
func (s *Service) Start() {
s.Running = make(chan bool)
}
// Close will stop the go routine that is checking if service is online or not
func (s *Service) Close() {
if s.IsRunning() {
close(s.Running)
}
}
// IsRunning returns true if the service go routine is running
func (s *Service) IsRunning() bool {
if s.Running == nil {
return false

View File

@ -23,11 +23,13 @@ import (
"math/rand"
)
// HashPassword returns the bcrypt hash of a password string
func HashPassword(password string) string {
bytes, _ := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes)
}
// NewSHA1Hash returns a random SHA1 hash based on a specific length
func NewSHA1Hash(n ...int) string {
noRandomCharacters := 32
if len(n) > 0 {
@ -51,6 +53,7 @@ func RandomString(n int) string {
return string(b)
}
// Sha256 returns a SHA256 hash as string from []byte
func Sha256(data []byte) string {
h := sha1.New()
h.Write(data)

View File

@ -38,6 +38,7 @@ func Logger() *lumberjack.Logger {
return ljLogger
}
// createLog will create the '/logs' directory based on a directory
func createLog(dir string) error {
var err error
_, err = os.Stat(dir + "/logs")
@ -56,6 +57,7 @@ func createLog(dir string) error {
return err
}
// InitLogs will create the '/logs' directory and creates a file '/logs/statup.log' for application logging
func InitLogs() error {
err := createLog(Directory)
if err != nil {
@ -90,6 +92,7 @@ func rotate() {
}()
}
// Log creates a new entry in the Logger. Log has 1-5 levels depending on how critical the log/error is
func Log(level int, err interface{}) error {
pushLastLine(err)
var outErr error
@ -122,6 +125,7 @@ func Log(level int, err interface{}) error {
return outErr
}
// Http returns a log for a HTTP request
func Http(r *http.Request) string {
msg := fmt.Sprintf("%v (%v) | IP: %v", r.RequestURI, r.Method, r.Host)
fmtLogs.Printf("WEB: %v\n", msg)
@ -140,6 +144,7 @@ func pushLastLine(line interface{}) {
}
}
// GetLastLine returns 1 line for a recent log entry
func GetLastLine() *LogRow {
LockLines.Lock()
defer LockLines.Unlock()

View File

@ -31,6 +31,7 @@ var (
Directory string
)
// init will set the utils.Directory to the current running directory, or STATUP_DIR if it is set
func init() {
if os.Getenv("STATUP_DIR") != "" {
Directory = os.Getenv("STATUP_DIR")
@ -39,15 +40,18 @@ func init() {
}
}
// StringInt converts a string to an int64
func StringInt(s string) int64 {
num, _ := strconv.Atoi(s)
return int64(num)
}
// IntString converts a int to a string
func IntString(s int) string {
return strconv.Itoa(s)
}
// dir returns the current working directory
func dir() string {
dir, err := os.Getwd()
if err != nil {
@ -57,16 +61,17 @@ func dir() string {
}
type Timestamp time.Time
type Timestamper interface {
Ago() string
}
// Ago returns a human readable timestamp based on the Timestamp (time.Time) interface
func (t Timestamp) Ago() string {
got, _ := timeago.TimeAgoWithTime(time.Now(), time.Time(t))
return got
}
// UnderScoreString will return a string that replaces spaces and other characters to underscores
func UnderScoreString(str string) string {
// convert every letter to lower case
@ -90,6 +95,7 @@ func UnderScoreString(str string) string {
return newStr
}
// FileExists returns true if a file exists
func FileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
@ -99,6 +105,7 @@ func FileExists(name string) bool {
return true
}
// DeleteFile will attempt to delete a file
func DeleteFile(file string) error {
Log(1, "deleting file: "+file)
err := os.Remove(file)
@ -108,10 +115,12 @@ func DeleteFile(file string) error {
return nil
}
// DeleteDirectory will attempt to delete a directory and all contents inside
func DeleteDirectory(directory string) error {
return os.RemoveAll(directory)
}
// Command will run a terminal command with 'sh -c COMMAND' and return stdout and errOut as strings
func Command(cmd string) (string, string, error) {
Log(1, "running command: "+cmd)
testCmd := exec.Command("sh", "-c", cmd)
@ -142,6 +151,7 @@ func Command(cmd string) (string, string, error) {
return outStr, errStr, err
}
// copyAndCapture will read a terminal command into bytes
func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) {
var out []byte
buf := make([]byte, 1024, 1024)