Skip to content

Commit

Permalink
Feature/windows support (#11)
Browse files Browse the repository at this point in the history
adds support for windows
  • Loading branch information
TimoKats authored Oct 19, 2024
1 parent e56efd1 commit e402b13
Show file tree
Hide file tree
Showing 14 changed files with 130 additions and 75 deletions.
11 changes: 2 additions & 9 deletions commands/commands.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
// Contains all the functions that can be invoked from the main package. Currently, that
// is: stop, list, run, start, and log. Hence, for each command there is a function. They
// use helper functions from the lib package. Sometimes, (e.g. in start) they also use
// functions from the setup submodule in this module.

package commands

import (
lib "github.com/TimoKats/pim/commands/lib"

"strconv"
"errors"
"os"
)
Expand All @@ -35,12 +29,11 @@ func ListCommand(process lib.Process, database *lib.Database) error {
dummySchedule := lib.CreateDummySchedule(process, database)
lib.Info.Println(lib.ViewListHeader())
for _, run := range process.Runs {
nextRun, runsCatchup := lib.ViewNextRun(dummySchedule, run)
nextRun, _ := lib.ViewNextRun(dummySchedule, run)
name := lib.ResponsiveWhitespace(run.Name)
cmd := lib.ResponsiveWhitespace(run.Command)
schedule := lib.ResponsiveWhitespace(run.Schedule)
duration := lib.ResponsiveWhitespace(strconv.Itoa(run.Duration))
lib.Info.Printf("%s | %s | %s | %s | %s | %v", name, schedule, cmd, duration, nextRun, runsCatchup)
lib.Info.Printf("%s | %s | %s | %s", name, schedule, cmd, nextRun)
}
return nil
}
Expand Down
5 changes: 0 additions & 5 deletions commands/flags.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
// Flags can be called from main package using a - or -- prefix. Currently, the Flags
// only output constant information (like license, version, etc) to standard output. The
// only reason they are in their own submodule is because it keeps other modules cleaner
// :)

package commands

import (
Expand Down
10 changes: 3 additions & 7 deletions commands/lib/consts.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
// This submodule contains all constant variables used in pim. Some are based on the home
// directory of the user. Hence, there are also some functions to derive that (no xgd).
// The const variables are: paths, helpstrings, and configuration settings. Note, use all
// caps format when defining consts.

package lib

import (
Expand Down Expand Up @@ -87,8 +82,9 @@ var DATAPATH string = CONFIGDIR + "data.yaml"
var LOCKPATH string = CONFIGDIR + "lockfile"
var CHECKPOINTPATH string = CONFIGDIR + "checkpoint"
var LOGPATH string = LOGDIR + DefaultLogPath()
var COLUMNWIDTH int = 25
var COMMANDS []string = []string{"ls", "run", "start", "stop", "clean", "stat"}
var COLUMNWIDTH int = 27
var PIMTERMINATE bool = false
var COMMANDS []string = []string{"ls", "run", "start", "stop", "clean", "status"}

// meta info
var VERSION string = "v0.0.1"
Expand Down
12 changes: 3 additions & 9 deletions commands/lib/cron.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
// Submodule that sets up the cron schedule. Uses external library called gocron. The
// Schedule variable is used as a 'public' variable to add cronjobs to. There are also
// some functions that are not related to this variable. For example, we allow running on
// startup (not a cron). These functions are also here. Finally, we also have functions
// for testing cron rather than executing it. Those functions contain "dummy" in their
// name.
//
// Note, Heartbeat function is there to keep pim running so that it can execute cron.

package lib

import (
Expand Down Expand Up @@ -85,6 +76,9 @@ func Heartbeat(process Process, database *Database) {
if checkpointErr := WriteCheckpoint(Schedule, RunJobMapping); checkpointErr != nil {
Error.Println(checkpointErr)
}
if PIMTERMINATE {
return
}
}
}

Expand Down
3 changes: 0 additions & 3 deletions commands/lib/database.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// Functions needed to store items in the database. Database itself is loaded/written in
// the io module.

package lib

import (
Expand Down
3 changes: 0 additions & 3 deletions commands/lib/io.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// All reading/writing operations to the filesystem are in this submodule. The categories
// of functions are: data.yaml, process.yaml, checkpoints, lockfiles.

package lib

import (
Expand Down
4 changes: 0 additions & 4 deletions commands/lib/logs.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
// Sets up logging format for pim. Note, if pim start is used, then switch to file logging.
// This prevents the user being interrupted by writing to standard output when running in
// the background.

package lib

import (
Expand Down
4 changes: 1 addition & 3 deletions commands/lib/runs.go → commands/lib/runs_linux.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// Module that executes the commands defined in process.yaml. Also has functions that write
// to temporary log files for timed runs. If something breaks in pim, it typically breaks
// here... :)
// +build linux

package lib

Expand Down
111 changes: 111 additions & 0 deletions commands/lib/runs_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// +build windows

package lib

import (
"context"
"os/exec"
"strings"
"syscall"
"errors"
"time"
"os"
)

func generateLogName(length int) string {
id := make([]byte, length)
for i := range id {
id[i] = IDCHARSET[SeededRand.Intn(len(IDCHARSET))]
}
return LOGDIR + "/" + string(id) + ".log"
}

func getCommandLogs(filename string) string {
content, fileErr := os.ReadFile(filename)
cmd := exec.Command("rm", filename)
defer cmd.Run() //nolint:errcheck
if fileErr != nil {
Error.Printf("Can't read from temp log file %s", filename)
return ""
}
return string(content)
}

func formatCommand(command string) (string, []string) {
var app string
var args []string
for index, text := range strings.Fields(command) {
if index == 0 {
app = text
} else {
args = append(args, text)
}
}
return app, args
}

func executeTimedRun(run Run, showOutput bool, duration int) (string, int) {
logName := generateLogName(5)
log, _ := os.Create(logName)
defer log.Close()
app, args := formatCommand(run.Command)
ctx, _ := context.WithTimeout(context.Background(), time.Duration(duration) * time.Second) //nolint:govet
cmd := exec.CommandContext(ctx, app, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP}
cmd.Dir = run.Directory
cmd.Env = os.Environ()
cmd.Stdout = log
if runErr := cmd.Run(); runErr != nil {
err = cmd.Process.Signal(syscall.SIGTERM) //nolint:errcheck
return getCommandLogs(logName), 0
}
return "not terminated", 1
}

func executeRun(run Run, showOutput bool) (string, int) {
var exitErr *exec.ExitError
app, args := formatCommand(run.Command)
cmd := exec.Command(app, args...)
cmd.Dir = run.Directory
runOutput, runErr := cmd.CombinedOutput()
if showOutput {
Info.Printf("%s", string(runOutput))
}
if errors.As(runErr, &exitErr) {
return string(runOutput), exitErr.ExitCode()
} else if runErr != nil {
return string(runOutput), -1
}
return string(runOutput), 0
}

func ExecuteCommand(command string) (string, int) {
var exitErr *exec.ExitError
app, args := formatCommand(command)
cmd := exec.Command(app, args...)
runOutput, runErr := cmd.CombinedOutput()
if errors.As(runErr, &exitErr) {
return string(runOutput), exitErr.ExitCode()
} else if runErr != nil {
return string(runOutput), -1
}
return string(runOutput), 0
}

func RunAndStore(run Run, database *Database, process Process, showOutput bool) {
var output string
var status int
if run.Duration != 0 {
output, status = executeTimedRun(run, showOutput, run.Duration)
} else {
output, status = executeRun(run, showOutput)
}
storedLog := StoreRun(run, output, status)
database.Logs = append(database.Logs, storedLog)
if (!process.OnlyStoreErrors) || (process.OnlyStoreErrors && status != 0) {
writeErr := WriteDataYaml(DATAPATH, *database)
if writeErr != nil {
Warn.Printf("process %s is not stored in database.", run.Name)
}
}
}
7 changes: 1 addition & 6 deletions commands/lib/stdout.go → commands/lib/tabular_output.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// Not sure what to call this module. It helps with formatting tables in standard output
// for a number of functions that I use elsewhere. pim log and pim ls use this.

package lib

import (
Expand Down Expand Up @@ -67,11 +64,9 @@ func ViewListHeader() string {
name := ResponsiveWhitespace("Name")
cronString := ResponsiveWhitespace("Cron string")
command := ResponsiveWhitespace("Command")
duration := ResponsiveWhitespace("Max duration")
nextRun := ResponsiveWhitespace("Next Run")
runStart := ResponsiveWhitespace("Runs on start")
return strings.Join(
[]string{name, cronString, command, duration, nextRun, runStart},
[]string{name, cronString, command, nextRun},
" | ",
)
}
Expand Down
6 changes: 0 additions & 6 deletions commands/lib/types.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
// Datatypes used in pim. Relates to yaml files: process.yaml, data.yaml, checkpoint.
//
// A semantic explanation of how the datatypes are used:
// A <process> consists of <run>s. A <run> creates a <log> that we put in the <database>.
// Checkpoints are created on heartbeats and are not related to the other data types.

package lib

import (
Expand Down
6 changes: 0 additions & 6 deletions commands/setup.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
// Commands that run every startup automatically. In short, the data file and yaml files
// are loaded (or created; if they don't exist yet) and parsed into objects through the
// SetupYamlFiles function. Next, certain attributes are cleaned and checked by helper
// functions. Finally, the CheckStartupErrors is called every startup to check if all
// const values that are needed can be set.

package commands

import (
Expand Down
9 changes: 0 additions & 9 deletions pim.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
// Contains the main control flow of the program. First, it's checked if there are any
// errors on startup. Next, it's checked if the correct number of parameters is provided.
// If this checks out also, the yaml files (process and database) are loaded. Also here,
// check for errors and return if need be.
//
// Finally, given the loaded yaml files and command parameters, the correct function is
// called in a case switch statement. If any errors occur, they are returned here.

package main

import (
Expand Down Expand Up @@ -67,4 +59,3 @@ func main() {
lib.Error.Println(parseErr)
}
}

14 changes: 9 additions & 5 deletions pim_test.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
// No tedious unit testing. Just run some of the main commands and see if anything breaks.
// Note, I want to add start here but I don't have a file system in GH actions for the
// lock files? So that fix is still coming.

package main

import (
lib "github.com/TimoKats/pim/commands/lib"
pim "github.com/TimoKats/pim/commands"

"testing"
"time"
)

var database lib.Database
var process = lib.Process{
Runs:[]lib.Run{
lib.Run{
Name: "linux-command",
Schedule: "@hourly",
Schedule: "@start+5",
Command: "echo hello world",
},
lib.Run{
Expand Down Expand Up @@ -64,3 +61,10 @@ func TestLog(t *testing.T) {
}
}

func TestStart(t *testing.T) {
go pim.StartCommand(process, &database) //nolint:errcheck
time.Sleep(10 * time.Second)
lib.Info.Println("pim start works!")
lib.PIMTERMINATE = true
}

0 comments on commit e402b13

Please sign in to comment.