cd~/go/src/sabey.co/patrol/unittest/testapp
go build -a -v
cd~/go/src/sabey.co/patrol/unittest/testserver
go build -a -v
cd~/go/src/sabey.co/patrol/
clear && go vet && go test -race
API
HTTP API Endpoint
GET /status/
# returns API_Status Object
GET /api/?group=(app||service)&id=testapp&toggle=STATE&history=true&secret=SECRET&cas=CAS
# returns API_Response Object
POST /api/
# requires API_Request Object# returns API_Response Object
UDP API Endpoint
127.0.0.1:1248
# requires API_Request Object# returns API_Response Object to the dialing IP address if no error occurred
// Apps/Services must contain a unique non empty key: ( 0-9 A-Z a-z - )// ID MUST be usable as a valid hostname label, ie: len <= 63 AND no starting/ending -// Keys are NOT our binary name// Keys are only used as unique identifiers for our API and Keep AliveAppsmap[string]*ConfigApp`json:"apps,omitempty"`Servicesmap[string]*ConfigService`json:"services,omitempty"`// TickEvery is an integer value in seconds of how often we will check the state of our Apps and Services// Value of 0 Defaults to 15 secondsTickEveryint`json:"tick-every,omitempty"`// History is the maximum amount of instance history we should hold// Value of 0 Defaults to 100Historyint`json:"history,omitempty"`// Timestamp Layout is used by the JSON API and HTTP GUI templates//// Timestamp Layout can be found here:// https://golang.org/pkg/time/#pkg-constants// https://golang.org/pkg/time/#example_Time_Format//// The recommended value is RFC1123Z: "Mon, 02 Jan 2006 15:04:05 -0700"//// An empty value will default to time.String()// https://golang.org/pkg/time/#Time.String// This default is: "2006-01-02 15:04:05.999999999 -0700 MST"// This default will also include our monotonic clock as a suffix: "m=±<value>"Timestampstring`json:"json-timestamp,omitempty"`// PingTimeout is an integer value in seconds of how often we require a Ping to be sent// This only applies to App KeepAlives: APP_KEEPALIVE_HTTP and APP_KEEPALIVE_UDPPingTimeoutint`json:"ping-timeout,omitempty"`// ListenHTTP/ListenUDP is our list of listeners// These values are passed as Environment Variables to our executed Apps as JSON Arrays//// Example Environment Variables:// PATROL_HTTP=["127.0.0.1:8421"]// PATROL_UDP=["127.0.0.1:1248"]//// When using APP_KEEPALIVE_HTTP and APP_KEEPALIVE_UDP, these are the addresses we MUST pingListenHTTP []string`json:"listen-http,omitempty"`ListenUDP []string`json:"listen-udp,omitempty"`// HTTP/UDP currently only support the attribute `listen`// This will allow us to overwrite our default listeners for HTTP and UDP// In the future this will include additional options.HTTP*ConfigHTTP`json:"http,omitempty"`UDP*ConfigUDP`json:"udp,omitempty"`// Triggers are only available when you extend Patrol as a library// These values will NOT be able to be set from `config.json` - They must be set manually// TriggerStart is called on CreatePatrol// This will only be called ONCE// If an error is returned a Patrol object will NOT be returned!TriggerStartfunc(
patrol*Patrol,
) error`json:"-"`// TriggerShutdown is called when we call Patrol.Shutdown()// This will only be called ONCE// Once Patrol.Shutdown() is called our Patrol object will no longer be usableTriggerShutdownfunc(
patrol*Patrol,
) `json:"-"`// TriggerStarted is called every time we call Patrol.Start()TriggerStartedfunc(
patrol*Patrol,
) `json:"-"`// TriggerTick is called every time we Patrol.tick() and BEFORE we check our App and Service StatesTriggerTickfunc(
patrol*Patrol,
) `json:"-"`// TriggerStopped is called every time we call Patrol.Stop()TriggerStoppedfunc(
patrol*Patrol,
) `json:"-"`// Extra Unstructured DataX json.RawMessage`json:"x,omitempty"`
type ConfigApp struct {
// KeepAlive Method//// APP_KEEPALIVE_PID_PATROL = 1// APP_KEEPALIVE_PID_APP = 2// APP_KEEPALIVE_HTTP = 3// APP_KEEPALIVE_UDP = 4//// PID_PATROL: Patrol will watch the execution of the Application. Apps will not be able to fork.// PID_APP: The Application is required to write its CURRENT PID to our `pid-path`. Patrol will `kill -0 PID` to verify that the App is running. This option should be used for forking processes.// HTTP: The Application must send a Ping request to our HTTP API.// UDP: The Application must send a Ping request to our UDP API.KeepAliveint`json:"keepalive,omitempty"`// Name is used as our Display Name in our HTTP GUI.// Name can contain any characters but must be less than 255 bytes in length.Namestring`json:"name,omitempty"`// Binary is the relative path to the executableBinarystring`json:"binary,omitempty"`// Working Directory is the ABSOLUTE Path to our Application Directory.// Binary, LogDirectory, and PidPath are RELATIVE to this Path!//// The only time WorkingDirectory is allowed to be relative is if we're prefixed with ~/// If prefixed with ~/, we will then replace it with our current users home directory.WorkingDirectorystring`json:"working-directory,omitempty"`// Log Directory is the relative path to our log directory.// STDErr and STDOut Logs are held in a `YEAR/MONTH/DAY` sub folder.LogDirectorystring`json:"log-directory,omitempty"`// Path is the relative path to our PID file.// PID is optional, it is only required when using the KeepAlive method: APP_KEEPALIVE_PID_APP// Our PID file must ONLY contain the integer of our current PIDPIDPathstring`json:"pid-path,omitempty"`// PIDVerify - Should we verify that our PID belongs to Binary?// PIDVerify is optional, it is only supported when using the KeepAlive method: APP_KEEPALIVE_PID_APP// This currently is NOT supported.// By default when we execute an App - `ps aux` will report our FULL PATH and BINARY as our first Arg.// If our process should fork, we're unsure of how this will change. We may have to compare that PID contains at the very least Binary in the first Arg.PIDVerifybool`json:"pid-verify,omitempty"`// If Disabled is true our App won't be executed until enabled.// The only way to enable an App once Patrol is started is to use the API or restart Patrol// If we are Disabled and we discover an App that is running, we will signal it to stop.Disabledbool`json:"disabled,omitempty"`// KeyValue - prexisting values to populate objects with on initKeyValuemap[string]interface{} `json:"keyvalue,omitempty"`// KeyValueClear if true will cause our App KeyValue to be cleared once a new instance of our App is started.KeyValueClearbool`json:"keyvalue-clear,omitempty"`// If Secret is set, we will require a secret to be passed when pinging and modifying the state of our App from our HTTP and UDP API.// We are not going to throttle comparing our secret. Choose a secret with enough bits of uniqueness and don't make your Patrol instance public!// If you are worried about your secret being public, use TLS and HTTP, DO NOT USE UDP!!!Secretstring`json:"secret,omitempty"`////////////// os.Cmd //////////////// ExecuteTimeout is an optional value in seconds of how long we will run our App for.// A Value of 0 will disable this.ExecuteTimeoutint`json:"execute-timeout,omitempty"`// Args holds command line arguments, including the command as Args[0].// If the Args field is empty or nil, Run uses {Path}.//// In typical use, both Path and Args are set by calling Command.Args []string`json:"args,omitempty"`// os.Cmd.Env specifies the environment of the process.// Each entry is of the form "key=value".// If os.Cmd.Env is nil, the new process uses the current process's environment.// If os.Cmd.Env contains duplicate environment keys, only the last value in the slice for each duplicate key is used.//// We're going to include our own Patrol related environment variables, so EnvParent is required if we wish to include parent values.Env []string`json:"env,omitempty"`// If EnvParent is true, we will prepend all of our Patrol environment variables to the execution of our process.EnvParentbool`json:"env-parent,omitempty"`// These options are only available when you extend Patrol as a library// These values will NOT be able to be set from `config.json` - They must be set manually// ExtraArgs is an optional set of values that will be appended to Args.ExtraArgsfunc(
idstring,
) []string`json:"-"`// ExtraEnv is an optional set of values that will be appended to Env.ExtraEnvfunc(
idstring,
) []string`json:"-"`// Stdin specifies the process's standard input.//// If Stdin is nil, the process reads from the null device (os.DevNull).//// If Stdin is an *os.File, the process's standard input is connected// directly to that file.//// Otherwise, during the execution of the command a separate// goroutine reads from Stdin and delivers that data to the command// over a pipe. In this case, Wait does not complete until the goroutine// stops copying, either because it has reached the end of Stdin// (EOF or a read error) or because writing to the pipe returned an error.Stdinio.Reader`json:"-"`// Stdout and Stderr specify the process's standard output and error.//// If either is nil, Run connects the corresponding file descriptor// to the null device (os.DevNull).//// If either is an *os.File, the corresponding output from the process// is connected directly to that file.//// Otherwise, during the execution of the command a separate goroutine// reads from the process over a pipe and delivers that data to the// corresponding Writer. In this case, Wait does not complete until the// goroutine reaches EOF or encounters an error.//// If Stdout and Stderr are the same writer, and have a type that can// be compared with ==, at most one goroutine at a time will call Write.//// If this value is nil, Patrol will create its own file located in our Log Directory.// If this value is nil, this file will also be able to be read from the HTTP GUI.
Stdout io.Writer`json:"-"`Stderr io.Writer`json:"-"`// Merge Stdout and Stderr into a single file?StdMergebool`json:"std-merge,omitempty"`// ExtraFiles specifies additional open files to be inherited by the// new process. It does not include standard input, standard output, or// standard error. If non-nil, entry i becomes file descriptor 3+i.ExtraFilesfunc(
idstring,
) []*os.File`json:"-"`// TriggerStart is called from tick in runApps() before we attempt to execute an App.TriggerStartfunc(
app*App,
) `json:"-"`// TriggerStarted is called from tick in runApps() and isAppRunning()// This is called after we either execute a new App or we discover a newly running App.TriggerStartedfunc(
app*App,
) `json:"-"`// TriggerStartedPinged is called from App.apiRequest() when we discover a newly running App from a Ping request.TriggerStartedPingedfunc(
app*App,
) `json:"-"`// TriggerStartFailed is called from tick in runApps() when we fail to execute a new App.TriggerStartFailedfunc(
app*App,
) `json:"-"`// TriggerRunning is called from tick() when we discover an App is running.TriggerRunningfunc(
app*App,
) `json:"-"`// TriggerDisabled is called from tick() when we discover an App that is disabled.TriggerDisabledfunc(
app*App,
) `json:"-"`// TriggerClosed is called from App.close() when we discover a previous instance of an App is closed.TriggerClosedfunc(
app*App,
history*History,
) `json:"-"`// TriggerPinged is from App.apiRequest() when we discover an App is running from a Ping request.TriggerPingedfunc(
app*App,
) `json:"-"`// TriggerShutdown is called when we call Patrol.Shutdown()// This will only be called ONCE// This is called regardless if our App is running or disabled!TriggerShutdownfunc(
app*App,
) `json:"-"`// Extra Unstructured DataX json.RawMessage`json:"x,omitempty"`
type ConfigService struct {
// Management Method//// SERVICE_MANAGEMENT_SERVICE = 1// SERVICE_MANAGEMENT_INITD = 2//// SERVICE_MANAGEMENT_SERVICE: Patrol will use the command `service *`// SERVICE_MANAGEMENT_INITD: Patrol will use the command `/etc/init.d/*`//// If Management is set it will ignore all of the Management Start/Status/Stop/Restart values// If Management is 0, Start/Status/Stop/Restart must each be individually set!// If for whatever reason is necessary, we could choose to user `service` for `status` and `/etc/init.d/` for start or stop!Managementint`json:"management,omitempty"`ManagementStartint`json:"management-start,omitempty"`ManagementStatusint`json:"management-status,omitempty"`ManagementStopint`json:"management-stop,omitempty"`ManagementRestartint`json:"management-restart,omitempty"`// Optionally we may override our service parameters.// For example, instead of `restart` we may choose to use `force-reload`ManagementStartParameterstring`json:"management-start-parameter,omitempty"`ManagementStatusParameterstring`json:"management-status-parameter,omitempty"`ManagementStopParameterstring`json:"management-stop-parameter,omitempty"`ManagementRestartParameterstring`json:"management-restart-parameter,omitempty"`// Name is used as our Display Name in our HTTP GUI.// Name can contain any characters but must be less than 255 bytes in length.Namestring`json:"name,omitempty"`// Service is the parameter of our service.// This is the equivalent of BinaryServicestring`json:"service,omitempty"`// These are a list of valid exit codes to ignore when returned from Start/Status/Stop/Restart// By Default 0 is always ignored, it is assumed to mean that the command was successful!IgnoreExitCodesStart []uint8`json:"ignore-exit-codes-start,omitempty"`IgnoreExitCodesStatus []uint8`json:"ignore-exit-codes-status,omitempty"`IgnoreExitCodesStop []uint8`json:"ignore-exit-codes-stop,omitempty"`IgnoreExitCodesRestart []uint8`json:"ignore-exit-codes-restart,omitempty"`// If Disabled is true our Service won't be executed until enabled.// The only way to enable an Service once Patrol is started is to use the API or restart Patrol// If we are Disabled and we discover an Service that is running, we will signal it to stop.Disabledbool`json:"disabled,omitempty"`// KeyValue - prexisting values to populate objects with on initKeyValuemap[string]interface{} `json:"keyvalue,omitempty"`// KeyValueClear if true will cause our Service KeyValue to be cleared once a new instance of our Service is started.KeyValueClearbool`json:"keyvalue-clear,omitempty"`// If Secret is set, we will require a secret to be passed when pinging and modifying the state of our Service from our HTTP and UDP API.// We are not going to throttle comparing our secret. Choose a secret with enough bits of uniqueness and don't make your Patrol instance public!// If you are worried about your secret being public, use TLS and HTTP, DO NOT USE UDP!!!Secretstring`json:"secret,omitempty"`// Triggers are only available when you extend Patrol as a library// These values will NOT be able to be set from `config.json` - They must be set manually// TriggerStart is called from tick in runServices() before we attempt to execute an Service.TriggerStartfunc(
service*Service,
) `json:"-"`// TriggerStarted is called from tick in runServices() and isServiceRunning()// This is called after we either execute a new Service or we discover a newly running Service.TriggerStartedfunc(
service*Service,
) `json:"-"`// TriggerStartFailed is called from tick in runServices() when we fail to execute a new Service.TriggerStartFailedfunc(
service*Service,
) `json:"-"`// TriggerRunning is called from tick() when we discover an Service is running.TriggerRunningfunc(
service*Service,
) `json:"-"`// TriggerDisabled is called from tick() when we discover an Service that is disabled.TriggerDisabledfunc(
service*Service,
) `json:"-"`// TriggerPinged is from Service.apiRequest() when we discover an Service is running from a Ping request.TriggerClosedfunc(
service*Service,
history*History,
) `json:"-"`// TriggerShutdown is called when we call Patrol.Shutdown()// This will only be called ONCE// This is called regardless if our Service is running or disabled!TriggerShutdownfunc(
service*Service,
) `json:"-"`// Extra Unstructured DataX json.RawMessage`json:"x,omitempty"`
type API_Status struct {
// Instance ID - UUIDv4InstanceIDstring`json:"instance-id,omitempty"`Appsmap[string]*API_Response`json:"apps,omitempty"`Servicesmap[string]*API_Response`json:"service,omitempty"`// Timestamp Patrol started atStartedstring`json:"started,omitempty"`// Is Patrol in a Shutdown state?Shutdownbool`json:"shutdown,omitempty"`
type API_Request struct {
// Requests by Default are STATELESS - If no values are set then nothing is modified!// The reason we're stateless by default is so that our UDP endpoint can make requests as if it were a HTTP GET Request// UDP has the downside that if an error occurs a response will not be sent in return// Unique IdentifierIDstring`json:"id,omitempty"`// Group: `app` or `service`Groupstring`json:"group,omitempty"`// Ping?// Only supported by either APP_KEEPALIVE_HTTP or APP_KEEPALIVE_UDP// If APP_KEEPALIVE_HTTP is used, the HTTP endpoint MUST be used// If APP_KEEPALIVE_UDP is used, the UDP endpoint MUST be usedPingbool`json:"ping,omitempty"`// App Process ID// Ping MUST be true if we wish to send a PIDPIDuint32`json:"pid,omitempty"`// Toggle State//// API_TOGGLE_STATE_ENABLE = 1// API_TOGGLE_STATE_DISABLE = 2// API_TOGGLE_STATE_RESTART = 3// API_TOGGLE_STATE_RUNONCE_ENABLE = 4// API_TOGGLE_STATE_RUNONCE_DISABLE = 5// API_TOGGLE_STATE_ENABLE_RUNONCE_ENABLE = 6// API_TOGGLE_STATE_ENABLE_RUNONCE_DISABLE = 7//// API_TOGGLE_STATE_ENABLE: Enable App or Service// API_TOGGLE_STATE_DISABLE: Disable App or Service// API_TOGGLE_STATE_RESTART: Restart App or Service, Enable App or Service if Disabled// API_TOGGLE_STATE_RUNONCE_ENABLE: Enable RunOnce for App or Service// API_TOGGLE_STATE_RUNONCE_DISABLE: Disable RunOnce for App or Service// API_TOGGLE_STATE_ENABLE_RUNONCE_ENABLE: Enable App or Service and Enable RunOnce// API_TOGGLE_STATE_ENABLE_RUNONCE_DISABLE: Enable App or Service and Disable RunOnceToggleuint8`json:"toggle,omitempty"`// Return History?Historybool`json:"history,omitempty"`// KeyValueKeyValuemap[string]interface{} `json:"keyvalue,omitempty"`// If KeyValueReplace is true, previous KeyValue will be replaced with KeyValueKeyValueReplacebool`json:"keyvalue-replace,omitempty"`// Secret is required to access the /api GET and POST endpointsSecretstring`json:"secret,omitempty"`// CAS IS OPTIONAL// if CAS is NOT set: we will ignore it and we will override all of our values and state!!!// if CAS IS SET: we will only override values if our CAS is correct!// HOWEVER, we will ALWAYS update our PING/LastSeen value REGARDLESS OF CAS!!!// updating `Ping, LastSeen, or PID` will cause our CAS to be incremented!!!CASuint64`json:"cas,omitempty"`
type API_Response struct {
// An API Response references our STATE at the time of Request// If any values change or CAS is incremented, they will STILL reference the premodification state!//// When using UDP, we won't be able to respond with all of our data, we're going to have to limit our response size// We're going to limit our response to: `id, group, pid, started, lastseen, disabled, restart, run-once, shutdown`// We'll have to ignore `history, keyvalue, and errors`, if they're needed the HTTP endpoint should be used instead// Unique IdentifierIDstring`json:"id,omitempty"`// Instance ID - UUIDv4 - Only exists IF we're running!InstanceIDstring`json:"instance-id,omitempty"`// Group: `app` or `service`Groupstring`json:"group,omitempty"`// Display NameNamestring`json:"name,omitempty"`// App Process IDPIDuint32`json:"pid,omitempty"`// Timestamp App or Service started atStartedstring`json:"started,omitempty"`// Timestamp App or Service was last seenLastSeenstring`json:"lastseen,omitempty"`// Is our App or Service Disabled?Disabledbool`json:"disabled,omitempty"`// Is our App or Service in a Restart state?Restartbool`json:"restart,omitempty"`// Is our App or Service set to RunOnce?RunOncebool`json:"run-once,omitempty"`// Is Patrol in a Shutdown state?Shutdownbool`json:"shutdown,omitempty"`// History of previous App or Service states at the time of close()History []*History`json:"history,omitempty"`// Current state's KeyValueKeyValuemap[string]interface{} `json:"keyvalue,omitempty"`// Does this App or Service require a Secret to modify?Secretbool`json:"secret,omitempty"`// Did any Errors occur?Errors []string`json:"errors,omitempty"`// like all of our other values, CAS is a snapshot of our PREVIOUS state// we are NEVER going to return our current CAS after modifying our current state or values// the reason for this is that if a modification request is successful, we know our CAS is CAS + 1// if we were to take a snapshot, update our object, then get our CAS ---// we could never actually verify what our current state or values are!!!// the reason for this has to do with triggers, we NEVER KNOW when we're going to unlock and/or execute triggers!!!// there are going to be very many scenarios where an API request is made and our CAS is updated more than once!!!// we're never in a scenario where we take a snapshot, update, and get our CAS WITHOUT UNLOCKING!!!// if we want to make a clean CAS, we should do a REQUEST without modifying anything(no ping), then do a secondary request without incrementing CAS!CASuint64`json:"cas,omitempty"`// CASInvalid is the only exception to data that references our previous snapshot// We need to know if our CAS was successful or not!// I prefer to have this as invalid and not valid as most requests without a CAS will be valid!CASInvalidbool`json:"cas-invalid,omitempty"`