Skip to content

Commit

Permalink
Add cronitor discover functionality for Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
jdotjdot committed May 20, 2022
1 parent b665920 commit 9bd826b
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 117 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ cronitor
*.key
ctab
cronitor-cli
cronitor.exe

TODO.md
local.md
223 changes: 217 additions & 6 deletions cmd/discover.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package cmd

import (
"github.com/cronitorio/cronitor-cli/lib"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"os/user"
"strings"

"github.com/capnspacehook/taskmaster"
"github.com/cronitorio/cronitor-cli/lib"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os"
"os/user"
"runtime"
"strings"
)

type ExistingMonitors struct {
Expand Down Expand Up @@ -140,7 +144,11 @@ Example where you perform a dry-run without any crontab modifications:
// Fetch list of existing monitor names for easy unique name validation and prompt prefill later on
existingMonitors.Monitors, _ = getCronitorApi().GetMonitors()

if len(args) > 0 {
if runtime.GOOS == "windows" {
if processWindowsTaskScheduler() {
importedCrontabs++
}
} else if len(args) > 0 {
// A supplied argument can be a specific file or a directory
if isPathToDirectory(args[0]) {
processDirectory(username, args[0])
Expand Down Expand Up @@ -201,6 +209,209 @@ func processDirectory(username, directory string) {
}
}

func getWindowsKey(taskName string) string {
const MonitorKeyLength = 12

h := sha256.New()
h.Write([]byte(taskName))
hashed := hex.EncodeToString(h.Sum(nil))
return hashed[:MonitorKeyLength]
}

type WrappedWindowsTask taskmaster.RegisteredTask

func NewWrappedWindowsTask(t taskmaster.RegisteredTask) WrappedWindowsTask {
w := WrappedWindowsTask(t)
return w
}

func (r WrappedWindowsTask) IsMicrosoftTask() bool {
return strings.HasPrefix(r.Path, "\\Microsoft\\")
}

func (r WrappedWindowsTask) GetCommandToRun() string {
var commands []string
for _, action := range r.Definition.Actions {

if action.GetType() != taskmaster.TASK_ACTION_EXEC {
// We only support actions of type Exec, not com, email, or message (which are deprecated)
continue
}

execAction := action.(taskmaster.ExecAction)

commands = append(commands, strings.TrimSpace(fmt.Sprintf("%s %s", execAction.Path, execAction.Args)))
}

return strings.Join(commands, " && ")
}

func processWindowsTaskScheduler() bool {
const CronitorWindowsPath = "C:\\Program Files\\cronitor.exe"

taskService, err := taskmaster.Connect()
if err != nil {
log(fmt.Sprintf("err: %v", err))
return false
}
defer taskService.Disconnect()
collection, err := taskService.GetRegisteredTasks()
if err != nil {
log(fmt.Sprintf("err: %v", err))
return false
}
defer collection.Release()

// Read crontab into map of Monitor structs
monitors := map[string]*lib.Monitor{}
monitorToRegisteredTask := map[string]taskmaster.RegisteredTask{}
for _, task := range collection {
t := NewWrappedWindowsTask(task)
// Skip all built-in tasks; users don't want to monitor those
if t.IsMicrosoftTask() {
continue
}

hostname, err := os.Hostname()
if err != nil {
log(fmt.Sprintf("err: %v", err))
}
// Windows Task Scheduler won't allow multiple tasks with the same name, so using
// the tasks' name should be safe. You also do not seem to be able to edit the name
// in Windows Task Scheduler, so this seems safe as the Key as well.
fullName := fmt.Sprintf("%s/%s", hostname, task.Name)
// Max name length of 75, so we need to truncate
if len(fullName) >= 74 {
fullName = fullName[:74]
}
defaultName := fullName
tags := createTags()
key := getWindowsKey(fullName)
name := defaultName
skip := false

// The monitor name will always be the same, so we don't have to fetch it
// from the Cronitor existing monitors

if !isAutoDiscover {
fmt.Println(fmt.Sprintf("\n %s %s", defaultName, t.GetCommandToRun()))
for {
prompt := promptui.Prompt{
Label: "Job name",
Default: name,
//Validate: validateName,
AllowEdit: name != defaultName,
Templates: promptTemplates(),
}

if result, err := prompt.Run(); err == nil {
name = result
} else if err == promptui.ErrInterrupt {
printWarningText("Skipped", true)
skip = true
break
} else {
printErrorText("Error: "+err.Error()+"\n", false)
}

break
}
}

if skip {
continue
}

existingMonitors.AddName(name)

notificationListMap := map[string][]string{}
if notificationList != "" {
notificationListMap = map[string][]string{"templates": {notificationList}}
}

monitor := lib.Monitor{
DefaultName: defaultName,
Key: key,
Rules: []lib.Rule{},
Platform: lib.WINDOWS,
Tags: tags,
Type: "heartbeat",
Notifications: notificationListMap,
NoStdoutPassthru: noStdoutPassthru,
}
tz := effectiveTimezoneLocationName()
if tz.Name != "" {
monitor.Timezone = tz.Name
}

monitors[key] = &monitor
monitorToRegisteredTask[key] = task
}

printLn()

if len(monitors) > 0 {
printDoneText("Sending to Cronitor", true)
}

monitors, err = getCronitorApi().PutMonitors(monitors)
if err != nil {
fatal(err.Error(), 1)
}

if !dryRun && len(monitors) > 0 {
for key, task := range monitorToRegisteredTask {
newDefinition := task.Definition
// Clear out all existing actions on the new definition
newDefinition.Actions = []taskmaster.Action{}
var actionList []taskmaster.Action
for _, action := range task.Definition.Actions {
if action.GetType() != taskmaster.TASK_ACTION_EXEC {
// We only support actions of type Exec, not com, email, or message (which are deprecated)

fmt.Printf("not exec: %v", action)

// We don't want to delete the old actions
actionList = append(actionList, action)
continue
}

execAction := action.(taskmaster.ExecAction)

// If the action has already been converted to use cronitor.exe, then we
// don't need to modify it
// TODO: What if cronitor.exe has been renamed?
if strings.HasSuffix(strings.ToLower(execAction.Path), "cronitor.exe") {
actionList = append(actionList, action)
continue
}

actionList = append(actionList, taskmaster.ExecAction{
ID: execAction.ID,
Path: CronitorWindowsPath,
Args: strings.TrimSpace(fmt.Sprintf("exec %s %s %s", key, execAction.Path, execAction.Args)),
WorkingDir: execAction.WorkingDir,
})
}
for _, action := range actionList {
newDefinition.AddAction(action)
}

output, _ := json.Marshal(newDefinition)
log(fmt.Sprintf("%s: %s", task.Path, output))

newTask, err := taskService.UpdateTask(task.Path, newDefinition)
if err != nil {
serialized, _ := json.Marshal(newTask)
log(fmt.Sprintf("err updating task %s: %v. JSON: %s", task.Path, err, serialized))
printWarningText(fmt.Sprintf("Could not update task %s to automatically ping Cronitor. Error: `%s`", task.Name, err), true)
}
}
}

return len(monitors) > 0
}

func processCrontab(crontab *lib.Crontab) bool {
defer printLn()
printSuccessText(fmt.Sprintf("Checking %s", crontab.DisplayName()), false)
Expand Down
10 changes: 9 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package cmd

import (
"github.com/cronitorio/cronitor-cli/lib"
"errors"
"fmt"
"github.com/cronitorio/cronitor-cli/lib"
"io/ioutil"
"math/rand"
"net/http"
Expand Down Expand Up @@ -243,6 +243,14 @@ func effectiveHostname() string {
}

func effectiveTimezoneLocationName() lib.TimezoneLocationName {

if runtime.GOOS == "windows" {
out, err := exec.Command("powershell", "-NoProfile", "-c", "(Get-TimeZone | Select-Object -First 1 -Property Id).Id | Write-Output").CombinedOutput()
if err == nil {
return lib.TimezoneLocationName{fmt.Sprintf("%s", out)}
}
}

// First, check if a TZ or CRON_TZ environemnt variable is set -- Diff var used by diff distros
if locale, isSetFlag := os.LookupEnv("TZ"); isSetFlag {
return lib.TimezoneLocationName{locale}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/cronitorio/cronitor-cli
go 1.14

require (
github.com/capnspacehook/taskmaster v0.0.0-20210519235353-1629df7c85e9
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/fatih/color v1.9.0
github.com/getsentry/raven-go v0.2.0
Expand Down
Loading

0 comments on commit 9bd826b

Please sign in to comment.