2019-03-12 19:24:06 +00:00
// bacillus - a A minimalist Build Automation/CI service
2019-03-12 20:22:07 +00:00
//
// bacillμs (Build Automation/Continuous Integration Low-Linecount μ(micro)-Service)
// listens for HTTP GET or POST events, executing specified actions on receipt of matching endpoint requests.
// Use it to respond to webhooks from SCM managers such as github, gitlab, gogs.io, etc.
// or from wget or curl requests made from git commit hooks.
//
// It is intended as a no-dependency, no-nonsense build automation system
// with minimal constraints so you may extend with whatever CI/CD/Devops process you want.
2019-01-20 05:02:52 +00:00
package main
import (
"context"
2019-09-12 23:11:17 +00:00
"encoding/json"
2019-01-20 05:02:52 +00:00
"flag"
"fmt"
"io"
"io/ioutil"
"log"
2019-01-20 19:51:22 +00:00
"math/rand"
2019-01-31 03:07:51 +00:00
"sort"
2019-08-29 03:26:41 +00:00
"time"
2019-01-26 21:43:45 +00:00
2019-01-27 22:53:55 +00:00
"net/http"
2019-01-20 05:02:52 +00:00
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
2019-03-14 03:00:27 +00:00
"blitter.com/go/brevity"
2019-08-29 03:26:41 +00:00
"blitter.com/go/moonphase"
2019-01-20 05:02:52 +00:00
)
const (
2019-09-06 02:41:29 +00:00
httpAuthUser = "bacuser"
httpAuthPasswd = "gramnegative" //b64:"YmFjdXNlcjpncmFtbmVnYXRpdmU="
2019-03-09 07:51:22 +00:00
//indStyleNone = "none"
indStyleIndent = "indent"
indStyleBoth = "both"
indStyleColour = "colour"
2019-01-20 05:02:52 +00:00
)
var (
2019-09-12 23:11:17 +00:00
version string
gitCommit string
2019-09-06 02:41:29 +00:00
2019-01-29 08:42:04 +00:00
server * http . Server
2019-03-12 19:24:06 +00:00
addrPort string // eg. ":9990"
basicAuth bool // flag: basic http auth
2019-01-31 07:47:09 +00:00
strUser string // API user
strPasswd string // API passwd
2019-01-29 08:42:04 +00:00
attachStdout bool
shutdownModeActive bool
killSwitch chan bool
2019-01-21 05:55:04 +00:00
//statUseUnicode bool
2019-01-31 03:07:51 +00:00
indStyle string
instCounter uint32
//runningJobCount uint
2019-03-13 07:52:36 +00:00
cmdMap map [ string ] string
runningJobs runningJobList //map[string]string
2019-03-15 05:43:01 +00:00
runningJobsLimit uint //max running jobs
2019-03-15 09:54:15 +00:00
demoMode bool // set to true to disable /shutdown and /rudeshutdown
2019-03-13 07:52:36 +00:00
jobHomeDir string
runLogTailLines int
showStagesOnFinished bool
2019-01-20 05:02:52 +00:00
2019-01-21 05:55:04 +00:00
//checkSeq string
//errSeq string
//playSeq string
2019-03-12 19:24:06 +00:00
// instColours is used for colouring job entry output if
// enabled, to aid in visually matching launch/completion
// entries
2019-02-08 05:32:45 +00:00
instColours = [ ] string {
"floralwhite" ,
"burlywood" ,
"cadetblue" ,
"chocolate" ,
"coral" ,
"cornflowerblue" ,
"cornsilk" ,
"darkcyan" ,
"darkgoldenrod" ,
"darkgrey" ,
"darkkhaki" ,
"darkorange" ,
"darksalmon" ,
"darkseagreen" ,
"darkturquoise" ,
"gainsboro" ,
"gold" ,
"goldenrod" }
2019-01-20 05:02:52 +00:00
)
2019-03-12 19:24:06 +00:00
// runningJobInfo stores some essential bits about a
// running job so they can be cancelled, and to track
// stages of jobs if they update their _stage files
// key: jobID
2019-03-06 18:28:12 +00:00
type runningJobInfo struct {
2019-03-12 19:24:06 +00:00
jobCanceller context . CancelFunc
jobTag string
workDir string
2019-03-06 18:28:12 +00:00
}
2019-03-12 19:24:06 +00:00
// runningJobList is the map of runningJobInfo entries
type runningJobList map [ string ] * runningJobInfo
// wrapper for io.WriteString() to ignore errors -- error-handling
// is useless to us for this application. Instead just log the error.
// Mostly to make gometalinter shut up.
func writeStr ( w io . Writer , s string ) {
_ , _ = io . WriteString ( w , s ) // nolint:errcheck
}
2019-01-31 03:07:51 +00:00
2019-01-26 02:59:18 +00:00
// There is a smattering of HTML and JS in this project, programmatically
// generated.
// No, I did not use templates.
// Yes, I may rewrite in the future to do so, but don't hold your breath.
// I didn't design this thing up-front, I wrote it to scratch an itch.
// That's what 'agile design' gets you :p
2019-03-12 19:24:06 +00:00
// xhrlinkCSSFrag emits CSS style used to mark job manual
// launch ('Play Job') entries in the dashboard.
2019-02-06 05:46:02 +00:00
func xhrlinkCSSFrag ( ) string {
2019-02-03 06:45:45 +00:00
return ` < style >
2019-02-20 02:42:35 +00:00
a . xhrlink {
text - decoration : none ;
color : inherit ;
2019-02-03 06:45:45 +00:00
}
2019-02-20 02:42:35 +00:00
a . xhrlink : visited {
color : inherit ;
2019-02-03 06:45:45 +00:00
}
2019-02-20 02:42:35 +00:00
a . xhrlink : hover {
text - decoration : underline ;
background - color : aliceblue ;
cursor : pointer ;
}
a . xhrlink : active {
background - color : lightgreen ;
}
< / style >
2019-02-03 06:45:45 +00:00
`
}
2019-03-12 19:24:06 +00:00
// xmlHTTPRequester emits a JS function suitable for calling from an
// html element. Typically used for an onclick event to fire off an
// async GET request.
2019-02-06 05:46:02 +00:00
func xmlHTTPRequester ( jsFuncName string , uri string , respHandlerJS string ) string {
2019-02-03 06:45:45 +00:00
return `
2019-03-13 07:52:36 +00:00
< ! -- ! A < audio id = ' jobRunSound ' type = ' audio / mpeg ' src = ' audio / 13280__ schademans__pipe1 . mp3 ' > < / audio > -- >
2019-02-03 06:45:45 +00:00
< script >
function ` + jsFuncName + ` ( ) {
// IDGAF about IE 5/6, nor should you
var xhttp = new XMLHttpRequest ( ) ;
xhttp . onreadystatechange = function ( ) {
if ( this . readyState == 4 && this . status == 200 ) {
2019-02-04 02:02:19 +00:00
// whatevs, maybe give feedback to user
` + respHandlerJS + `
2019-02-03 06:45:45 +00:00
}
} ;
xhttp . open ( ' GET ' , ' ` + uri + ` ' , true ) ;
xhttp . send ( ) ;
}
< / script > `
}
2019-03-12 19:24:06 +00:00
// xhrRunningJobsCountHandler emits a string representation of the
// number of currently running jobs.
2019-02-06 05:46:02 +00:00
func xhrRunningJobsCountHandler ( w http . ResponseWriter , r * http . Request ) {
2019-03-12 19:24:06 +00:00
writeStr ( w , fmt . Sprintf ( "%d" , len ( runningJobs ) ) )
2019-02-06 05:46:02 +00:00
}
2019-01-31 07:47:09 +00:00
2019-03-12 19:24:06 +00:00
// xhrLiveRunLogHandler emits an HTML fragment containing the specified
// number of runlog entries.
// params: r.URL.Query()["tl"][0] - uint, # of entries to yield
2019-02-06 05:46:02 +00:00
func xhrLiveRunLogHandler ( w http . ResponseWriter , r * http . Request ) {
2019-03-06 18:54:24 +00:00
tl := 6
2019-02-06 05:46:02 +00:00
v , ok := r . URL . Query ( ) [ "tl" ]
if ok {
2019-03-12 19:24:06 +00:00
fmt . Sscanf ( v [ 0 ] , "%d" , & tl ) // nolint:errcheck
2019-01-31 07:47:09 +00:00
}
2019-03-12 19:24:06 +00:00
writeStr ( w , liveRunLogHTML ( tl ) ) // nolint:errcheck
2019-01-31 07:47:09 +00:00
}
2019-03-12 19:24:06 +00:00
// favIconHTML emits an HTML fragment with the page's favIcon.
2019-02-06 05:46:02 +00:00
func favIconHTML ( ) string {
2019-01-27 21:20:40 +00:00
return ` <link rel="icon" type="image/jpg" href="/images/logo.jpg"/> `
}
2019-03-12 19:24:06 +00:00
// logoShortHdrHTML emits an HTML fragment with the project logo/name/version
2019-02-06 05:46:02 +00:00
func logoShortHdrHTML ( ) string {
2019-09-06 02:41:29 +00:00
return ` <img style='float:left;' width='16' src='/images/logo.jpg'/><pre><a href='/'>bacillμs ` + version + ` </a></pre> `
2019-02-06 05:46:02 +00:00
}
2019-03-12 19:24:06 +00:00
// logoShortHdrHTML emits an HTML fragment with the project logo/name/version
// and a link to the project's homepage.
2019-02-06 05:46:02 +00:00
func logoHdrHTML ( ) string {
2019-09-06 02:41:29 +00:00
return ` <img style='float:left;' width='16' src='/images/logo.jpg'/><pre><a href='/'>bacillμs ` + version + ` <a target='_' href='https://gogs.blitter.com/Russtopia/bacillus/src/master/README.md'>(What's this?)</a></pre> `
2019-02-06 05:46:02 +00:00
}
2019-03-12 19:24:06 +00:00
// bodyBgndHTMLAttribs emits an HTML fragment specifying the CSS background
// for the page.
2019-02-06 05:46:02 +00:00
func bodyBgndHTMLAttribs ( ) string {
2019-01-29 08:42:04 +00:00
if shutdownModeActive {
return ` style='background: linear-gradient(to bottom, rgba(0,0,0,0.1) 0%,rgba(0,0,0,0.8) 100%); background-image: url("/images/bacillus-shutdown.jpg"); background-size: cover;' `
}
2019-01-27 05:59:34 +00:00
return ` style='background: linear-gradient(to bottom, rgba(0,0,0,0.1) 0%,rgba(0,0,0,0.8) 100%); background-image: url("/images/bacillus.jpg"); background-size: cover;' `
2019-01-26 02:59:18 +00:00
}
2019-02-06 05:46:02 +00:00
// goBackJS() returns a JS fragment to make a page go back after a
2019-03-12 19:24:06 +00:00
// specified delay.
2019-02-21 05:38:07 +00:00
func goBackJS ( pages , ms string ) string {
2019-01-24 05:42:02 +00:00
return fmt . Sprintf ( `
< script >
// Go back after a short delay
2019-02-21 05:38:07 +00:00
setInterval ( function ( ) { /*window.location.href = document.referrer;*/ window . history . go ( - % s ) ; } , % s ) ;
2019-01-24 05:42:02 +00:00
< / script >
2019-02-21 05:38:07 +00:00
` , pages , ms )
2019-01-24 05:42:02 +00:00
}
2019-03-12 19:24:06 +00:00
// refreshMetaTag returns an HTML fragment defining the page refresh interval.
2019-03-04 06:20:02 +00:00
func refreshMetaTag ( stat rune , intervalSecs string ) string {
2019-01-20 05:02:52 +00:00
if stat == 'r' {
2019-01-31 03:07:51 +00:00
return ` <meta http-equiv="refresh" content=" ` + intervalSecs + ` "> `
2019-01-20 05:02:52 +00:00
}
2019-03-09 07:51:22 +00:00
return ` `
2019-01-20 05:02:52 +00:00
}
2019-03-04 06:20:02 +00:00
// forceReloadOnHistJS() emits a JS fragment suitable for inclusion into
// HTML page <head>ers that forces a page refresh if the page is visited
// via the browser history or back button.
func forceReloadOnHistJS ( ) string {
return ` < script >
if ( performance . navigation . type == 2 ) {
location . reload ( true ) ;
}
< / script >
`
}
2019-03-12 19:24:06 +00:00
// consActiveSpinnerCSS returns a CSS fragment defining the appearance
// and behaviour of a spinner if the enclosing page defines an element
// with ids #spinner, #finOKMarker and #finErrMarker.
2019-02-06 05:46:02 +00:00
func consActiveSpinnerCSS ( ) string {
2019-01-20 05:02:52 +00:00
return `
< style >
# spinner {
position : fixed ;
2019-03-05 04:44:44 +00:00
right : 0.5 em ; bottom : 1 em ;
2019-01-20 05:02:52 +00:00
font - family : monospace ;
2019-03-05 04:44:44 +00:00
margin : 0.5 em ;
2019-01-20 05:02:52 +00:00
padding : 0.2 em ;
font - size : 1.5 em ;
font - weight : normal ; //bold;
background : skyblue ;
border : dotted 2 px ;
2019-03-05 04:44:44 +00:00
border - radius : 0.5 em ;
2019-01-20 05:02:52 +00:00
}
# finOKMarker {
position : fixed ;
2019-03-05 04:44:44 +00:00
right : 0.5 em ; bottom : 1 em ;
2019-01-20 05:02:52 +00:00
font - family : monospace ;
2019-03-05 04:44:44 +00:00
margin : 0.5 em ;
2019-01-20 05:02:52 +00:00
padding : 0.2 em ;
font - size : 1.5 em ;
font - weight : normal ;
background : lightgreen ;
border : dotted 2 px ;
2019-03-05 04:44:44 +00:00
border - radius : 0.5 em ;
2019-01-20 05:02:52 +00:00
}
# finErrMarker {
position : fixed ;
2019-03-05 04:44:44 +00:00
right : 0.5 em ; bottom : 1 em ;
2019-01-20 05:02:52 +00:00
font - family : monospace ;
2019-03-05 04:44:44 +00:00
margin : 0.5 em ;
2019-01-20 05:02:52 +00:00
padding : 0.2 em ;
font - size : 1.5 em ;
font - weight : bold ;
background : red ;
border : dotted 2 px ;
2019-03-05 04:44:44 +00:00
border - radius : 0.5 em ;
2019-01-20 05:02:52 +00:00
}
//#stat {
// display: none;
//}
< / style >
`
}
2019-03-12 19:24:06 +00:00
// consActiveSpinnerJS returns JS code to animate a spinner element on
// the enclosing page, having a DOM id of #spinner.
2019-02-06 05:46:02 +00:00
func consActiveSpinnerJS ( stat rune , codeColor , statWord string ) string {
if stat == 'r' {
return ` < script >
////////////////////////
appendSpinner = function ( ) {
var spinners = [
"|/-\\" ,
".oO@*" ,
[ ">))'>" , " >))'>" , " >))'>" , " >))'>" , " >))'>" , " <'((<" , " <'((<" , " <'((<" ] ,
] ;
var el = document . createElement ( ' div ' ) ;
el . setAttribute ( ' id ' , ' spinner ' ) ;
document . body . appendChild ( el ) ;
el . innerHTML = '.' ;
var spinner = spinners [ 0 ] ;
( function ( spinner , el ) {
var i = 0 ;
setInterval ( function ( ) {
el . innerHTML = spinner [ i ] ;
i = ( i + 1 ) % spinner . length ;
} , 300 ) ;
} ) ( spinner , el ) ;
}
////////////////////////
< / script > `
2019-03-09 07:51:22 +00:00
}
return ` < script >
2019-02-06 05:46:02 +00:00
////////////////////////
appendSpinner = function ( ) {
var el = document . createElement ( ' div ' ) ;
el . setAttribute ( ' id ' , ' ` + codeColor + ` ' ) ;
el . innerHTML = ' ` + statWord + ` ' ;
document . body . appendChild ( el ) ;
}
////////////////////////
< / script > `
}
2019-03-12 19:24:06 +00:00
// compatJS emits a JS fragment to return the proper cross-browser version
// of document.scrollingElement, used to set the vertical scroll position
// of the current page.
2019-02-06 05:46:02 +00:00
func compatJS ( ) string {
2019-01-20 05:02:52 +00:00
return `
< script >
bodyOrHtml = function ( ) {
if ( ' scrollingElement ' in document ) {
return document . scrollingElement ;
}
// Fallback for legacy browsers
if ( navigator . user - Agent . indexOf ( ' WebKit ' ) != - 1 ) {
return document . body ;
}
return document . documentElement ;
}
scrollDown = function ( ) {
setTimeout ( function ( ) {
bodyOrHtml ( ) . scrollTop = bodyOrHtml ( ) . scrollHeight ;
} , 5 ) ; // hack: delay due to most browsers' auto-scroll reset on page reload
}
< / script >
`
}
2019-03-12 19:24:06 +00:00
// liveRunLogHTML returns the HTML for 'live' runlog with a specified # of
// tail lines, updated to reflect the current running status of pending jobs.
// The output is meant to be inserted within an enclosing, complete HTML page.
// (For just an HTML fragment of some specific # of most recent entries,
// to be inserted by client-side, see xurLiveRunLogHandler())
2019-02-06 05:46:02 +00:00
func liveRunLogHTML ( tl int ) ( ret string ) {
2019-02-02 05:36:57 +00:00
rl , _ := ioutil . ReadFile ( fmt . Sprintf ( "run%s.log" , strings . Split ( addrPort , ":" ) [ 1 ] ) )
// Split log into header and the rest, with endpoints
// at top and events below, so as log gets longer user
// can still see important bits.
lines := strings . Split ( string ( rl ) , "--BACILLUS READY--" )
tailLines := strings . Split ( lines [ 1 ] , "\n" )
tailCount := len ( tailLines )
// Scan backwards in log for completion msgs, match with
// preceding launch msgs to un-mark the in-progress and cancel icons there
// (only 'live' view)
2019-03-12 20:22:07 +00:00
tailLines = patchLiveViewOfRunLogEntries ( tailLines , tl )
2019-02-02 05:36:57 +00:00
if tl == 0 || tailCount < tl {
ret += strings . Join ( tailLines , "\n" )
} else {
ret += strings . Join ( tailLines [ tailCount - tl : ] , "\n" )
}
2019-02-22 06:22:36 +00:00
ret = strings . TrimPrefix ( ret , "\n" )
ret = strings . TrimSuffix ( ret , "\n" )
2019-02-02 05:36:57 +00:00
return
}
2019-03-12 19:24:06 +00:00
// manualJobTriggersJS returns a JS fragment for each defined job,
// meant to be bound to be onclick event of their corresponding
// 'Play Job' links in the dashboard or full runlog pages
// See manualJobTriggersHTML()
2019-02-06 05:46:02 +00:00
func manualJobTriggersJS ( ) ( ret string ) {
2019-03-12 22:22:49 +00:00
// sort the job keys
2019-02-06 05:46:02 +00:00
keys := make ( [ ] string , len ( cmdMap ) )
for k := range cmdMap {
keys = append ( keys , k )
}
sort . Strings ( keys )
2019-03-12 19:24:06 +00:00
// For each endpoint 'fn', add a JS fragment which is a
// function which will be bound elsewhere to the onclick
// event of the job's link in the manual job trigger section
// of the page: see manualJobTriggersHTML().
2019-02-06 05:46:02 +00:00
for _ , k := range keys {
if len ( cmdMap [ k ] ) > 0 {
fn := strings . Replace ( k , "-" , "" , - 1 )
ret += xmlHTTPRequester ( fn , k , "" )
ret += ` < script >
2019-03-13 07:52:36 +00:00
setInterval ( xhrLiveRunLogUpdate , 2000 ) ;
setInterval ( xhrRunningJobsCount , 2000 ) ;
2019-02-06 05:46:02 +00:00
< / script > `
}
}
return
}
2019-03-12 19:24:06 +00:00
func hasParameterSpecifier ( line string ) bool {
if strings . HasPrefix ( line , "#-?" ) ||
strings . HasPrefix ( line , "/*-?" ) ||
strings . HasPrefix ( line , "//*-?" ) {
return true
}
return false
}
// isParameterizedBuildScript scans job script scriptFName for build
// parameter form specifiers.
// It returns false if there are none, otherwise true
2019-02-20 02:42:35 +00:00
func isParameterizedBuildScript ( scriptFName string ) bool {
2019-02-20 05:24:55 +00:00
isParamJob := false
fileBytes , e := ioutil . ReadFile ( jobHomeDir + strings . TrimPrefix ( scriptFName , ".." ) )
if e != nil {
fmt . Println ( "Error:" , e )
2019-02-20 02:42:35 +00:00
return false
}
2019-02-20 05:24:55 +00:00
lines := strings . Split ( string ( fileBytes ) , "\n" )
for _ , line := range lines {
2019-03-12 19:24:06 +00:00
if hasParameterSpecifier ( line ) {
2019-02-20 05:24:55 +00:00
isParamJob = true
} else if isParamJob {
break
}
}
return isParamJob
}
2019-03-12 19:24:06 +00:00
// genParameterizedBuildForm scans job script scriptFName for build parameter
// form specifiers, returning an HTML fragment suitable for setting all
// defined parameters.
2019-03-12 20:22:07 +00:00
//
// nolint:gocyclo
2019-02-22 06:22:36 +00:00
func genParameterizedBuildForm ( jobTag , scriptFName string ) ( ret string ) {
2019-02-20 05:24:55 +00:00
paramJobLine := false
2019-02-21 05:38:07 +00:00
scriptFName = strings . TrimPrefix ( scriptFName , "../" )
fileBytes , e := ioutil . ReadFile ( jobHomeDir + "/" + scriptFName )
2019-02-20 05:24:55 +00:00
if e != nil {
fmt . Println ( "Error:" , e )
return
}
lines := strings . Split ( string ( fileBytes ) , "\n" )
for _ , line := range lines {
// TODO: parse lines for "#-?" entries, build
// HTML page w/form to set params and pass to job
// via a submit link
2019-03-12 19:24:06 +00:00
if hasParameterSpecifier ( line ) {
2019-02-21 05:38:07 +00:00
if ! paramJobLine {
2019-03-12 19:24:06 +00:00
// First entry, build form prologue
2019-02-21 05:38:07 +00:00
// The hidden ?paramSet will trigger
// the final stage of same endpoint that
// calls this func (launchJob)
//
// TODO: form action="%s"
ret += `
< h2 > ` + jobTag + ` < / h2 >
< h3 > Build with Parameters < / h3 >
< hr / >
< form action = "/` + jobTag + `?paramSet" method = "GET" >
< input type = "hidden" name = "paramSet" / >
`
}
2019-02-20 05:24:55 +00:00
paramJobLine = true
2019-02-21 05:38:07 +00:00
// Determine type of build param
// [0]:paramMarker (#-?) [1]:type (b|c|s) [2]:name [3]:(vals ...)
paramFields := strings . Split ( line , "?" )
2019-02-22 06:22:36 +00:00
var paramComment string
if len ( paramFields ) > 4 {
// has comment
paramComment = paramFields [ 4 ]
}
2019-02-21 05:38:07 +00:00
switch paramFields [ 1 ] {
case "s" :
2019-02-22 06:22:36 +00:00
ret += fmt . Sprintf ( "%s:<input type='text' name='%s' value='%s' /> %s<br />\n" ,
paramFields [ 2 ] , paramFields [ 2 ] , paramFields [ 3 ] ,
paramComment )
2019-02-21 05:38:07 +00:00
case "c" :
choices := strings . Split ( paramFields [ 3 ] , "|" )
ret += paramFields [ 2 ] + ":<select name='" + paramFields [ 2 ] + "'>\n"
for _ , c := range choices {
ret += " <option value='" + c + "'>" + c + "</option>\n"
}
2019-02-22 06:22:36 +00:00
ret += "</select> " + paramComment + "<br />\n"
2019-02-21 05:38:07 +00:00
case "b" :
// NOTE the 'b' bool type uses HTML input type='checkbox'
// which sends nothing if unset. (eg., the job should
// expect a missing param and assume that means 'false',
// 'off', 'disabled' ...
//
// In bash syntax that would typically be handled like:
// option=${option:-"false"}
ret += paramFields [ 2 ]
ret += " <input type='checkbox' name='" + paramFields [ 2 ] + "'"
if paramFields [ 3 ] == "on" ||
paramFields [ 3 ] == "true" ||
paramFields [ 3 ] == "1" ||
strings . HasPrefix ( paramFields [ 3 ] , "enable" ) {
ret += "value='true' checked"
} // else {
// ret += "value='false'"
//}
2019-02-22 06:22:36 +00:00
ret += "/> " + paramComment + "<br />\n"
2019-02-21 05:38:07 +00:00
}
2019-02-20 05:24:55 +00:00
} else if paramJobLine {
2019-03-12 19:24:06 +00:00
// End of param specifiers, emit form epilogue
2019-02-21 05:38:07 +00:00
ret += `
< input type = "submit" value = "Build" / >
2019-02-22 06:22:36 +00:00
< / form > `
2019-02-20 05:24:55 +00:00
break
}
}
2019-02-21 05:38:07 +00:00
return ret
2019-02-20 02:42:35 +00:00
}
2019-08-29 03:26:41 +00:00
func sayingFooterHTML ( ) ( ret string ) {
prefix := ` <pre style='font-size: 8px; position: fixed; bottom: 0px; right: 10px;'> `
suffix := ` </pre> `
t := time . Now ( )
m := moonphase . New ( t )
2019-09-07 23:19:32 +00:00
n := m . PhaseSymbol ( )
2019-08-29 03:26:41 +00:00
footerMain := ""
switch n {
case "New Moon" :
2019-09-07 23:19:32 +00:00
footerMain = n + " It is pitch dark. You are likely to be eaten by a Grue."
2019-08-29 03:26:41 +00:00
case "Full Moon" :
2019-09-07 23:19:32 +00:00
footerMain = n + " Watch out! Full moon tonight."
2019-08-29 03:26:41 +00:00
default :
2019-09-12 23:11:17 +00:00
footerMain = fmt . Sprintf ( "%s " , n ) + ` Best viewed using DejaVu font family --- Qui verifiers ratum efficiat? Non I. `
2019-08-29 03:26:41 +00:00
}
ret = prefix + footerMain + suffix
return
}
2019-03-12 19:24:06 +00:00
// manualJobTriggersHTML returns an HTML fragment containing href links to
// all defined job endpoints. Note manualJobTriggersJS() must be used
// in conjunction with this output to bind onclick handlers to them.
2019-02-06 05:46:02 +00:00
func manualJobTriggersHTML ( fullLogLink bool ) ( ret string ) {
ret = "<pre style='background-color: skyblue;'>"
keys := make ( [ ] string , len ( cmdMap ) )
for k := range cmdMap {
keys = append ( keys , k )
}
sort . Strings ( keys )
for _ , k := range keys {
if len ( cmdMap [ k ] ) > 0 {
2019-02-20 02:42:35 +00:00
// ===================
2019-02-21 05:38:07 +00:00
// Examine script at (k) for job-param syntax:
2019-02-20 02:42:35 +00:00
// If present, gen code to go to a params page dynamically
// constructed w/param form, rather than a direct XHR to
// launch endpoint
// ===================
2019-03-29 06:30:39 +00:00
if _ , e := os . Stat ( strings . Replace ( cmdMap [ k ] , ".." , jobHomeDir , - 1 ) ) ; e != nil {
ret += fmt . Sprintf ( "-- job script %s not found --\n" , cmdMap [ k ] )
2019-02-20 02:42:35 +00:00
} else {
2019-03-29 06:30:39 +00:00
if isParameterizedBuildScript ( cmdMap [ k ] ) {
ret += fmt . Sprintf ( "<a class='xhrlink' title='Play Job with Parameters' href='%s?param'>[▹] %s [action %s]</a>\n" ,
k , k , cmdMap [ k ] )
} else {
fn := strings . Replace ( k , "-" , "" , - 1 )
ret += fmt . Sprintf ( ` <a class='xhrlink' onclick='%s(); return false;' title='Play Job' href='%s'>[▸] %s [action %s]</a> ` + "\n" ,
fn , k , k , cmdMap [ k ] )
}
2019-02-20 02:42:35 +00:00
}
2019-02-06 05:46:02 +00:00
}
}
if fullLogLink {
ret += "<a href='/fullrunlog'>... click for full runlog ...</a>"
}
ret += "</pre>"
return
}
2019-03-12 19:24:06 +00:00
// httpAuthSession should be used at the start of all endpoints to
// enforce basic HTTP auth (this function is a nop if auth is disabled
// in server config). Returns true if user is authorized, else
// a 'Not logged in' page is sent to the client and false is returned.
//
// NOTE basic auth is not secure by itself; the user/password are
// sent to the server in plaintext unless TLS protects the server.
// A reverse proxy enforcing HTTPS on the server is highly recommended.
2019-02-06 05:46:02 +00:00
func httpAuthSession ( w http . ResponseWriter , r * http . Request ) ( auth bool ) {
w . Header ( ) . Set ( "Cache-Control" , "no-cache" )
if ! basicAuth {
return true
}
u , p , ok := r . BasicAuth ( )
if ok && u == strUser && p == strPasswd {
return true
}
2019-03-09 07:51:22 +00:00
w . Header ( ) . Set ( "WWW-Authenticate" , ` Basic realm="Bacillus" ` )
w . WriteHeader ( http . StatusUnauthorized )
2019-03-12 19:24:06 +00:00
writeStr ( w , "Not logged in." ) // nolint:errcheck
2019-02-06 05:46:02 +00:00
return
}
// TODO: types for matching JSON events of
// supported webhooks: gogs.io, github, gitlab, ... ?
// For now, the 'blind' endpoint is the only one supported,
// meaning the request can't communicate any extra data to the
// job invocation in a GET or POST request.
2019-02-02 05:36:57 +00:00
func runLogHandler ( w http . ResponseWriter , r * http . Request ) {
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/html;charset=UTF-8" )
2019-02-02 05:36:57 +00:00
if ! httpAuthSession ( w , r ) {
return
}
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-02-02 05:36:57 +00:00
< html >
2019-02-04 02:02:19 +00:00
< head > ` +
2019-02-06 05:46:02 +00:00
favIconHTML ( ) +
xhrlinkCSSFrag ( ) +
2019-02-08 05:32:45 +00:00
xmlHTTPRequester ( "xhrLiveRunLogUpdate" , fmt . Sprintf ( "/api/lru?tl=%d" , runLogTailLines ) , ` document.getElementById('liveRunLog').innerHTML = xhttp.response; ` ) +
2019-02-06 05:46:02 +00:00
logoShortHdrHTML ( ) + `
2019-02-02 05:36:57 +00:00
< / head >
2019-02-06 05:46:02 +00:00
< body ` +bodyBgndHTMLAttribs()+ ` > ` )
2019-03-12 19:24:06 +00:00
writeStr ( w , manualJobTriggersHTML ( true ) +
2019-02-06 05:46:02 +00:00
` <pre id='liveRunLog'> ` + liveRunLogHTML ( runLogTailLines ) + ` </pre> ` )
2019-02-02 05:36:57 +00:00
2019-03-12 19:24:06 +00:00
writeStr ( w , manualJobTriggersJS ( ) )
writeStr ( w , `
2019-02-02 05:36:57 +00:00
< / body >
< / html >
` )
}
2019-01-20 05:02:52 +00:00
2019-01-21 05:55:04 +00:00
func fullRunlogHandler ( w http . ResponseWriter , r * http . Request ) {
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/html;charset=UTF-8" )
2019-01-31 07:47:09 +00:00
if ! httpAuthSession ( w , r ) {
return
}
2019-01-24 08:48:18 +00:00
runLog , e := ioutil . ReadFile ( fmt . Sprintf ( "run%s.log" , strings . Split ( addrPort , ":" ) [ 1 ] ) )
2019-01-21 05:55:04 +00:00
if e != nil {
2019-03-12 19:24:06 +00:00
writeStr ( w , fmt . Sprintf ( "%s" , e ) )
2019-01-21 05:55:04 +00:00
return
}
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-02-02 05:36:57 +00:00
< html >
< head > ` +
2019-02-06 05:46:02 +00:00
favIconHTML ( ) +
logoShortHdrHTML ( ) + `
2019-02-02 05:36:57 +00:00
< / head >
2019-03-12 19:24:06 +00:00
< body > ` ) // nolint:errcheck
2019-02-02 05:36:57 +00:00
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-02-02 05:36:57 +00:00
< pre >
2019-02-22 06:22:36 +00:00
` +string(runLog)+ ` < / pre >
2019-02-02 05:36:57 +00:00
< / body >
2019-03-12 19:24:06 +00:00
< / html > ` ) // nolint:errcheck
2019-01-21 05:55:04 +00:00
}
2019-01-20 05:02:52 +00:00
func fullConsoleHandler ( w http . ResponseWriter , r * http . Request ) {
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/plain;charset=UTF-8" )
2019-01-31 07:47:09 +00:00
if ! httpAuthSession ( w , r ) {
return
}
2019-03-12 19:24:06 +00:00
consoleLog , e := ioutil . ReadFile ( strings . Replace ( r . URL . String ( ) [ 1 : ] , "/fullconsole" , "" , 1 ) )
2019-01-20 05:02:52 +00:00
if e != nil {
2019-03-12 19:24:06 +00:00
writeStr ( w , fmt . Sprintf ( "%s" , e ) )
2019-01-20 05:02:52 +00:00
return
}
2019-03-12 19:24:06 +00:00
writeStr ( w , string ( consoleLog ) )
2019-01-20 05:02:52 +00:00
}
func consoleHandler ( w http . ResponseWriter , r * http . Request ) {
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/html;charset=UTF-8" )
2019-01-31 07:47:09 +00:00
if ! httpAuthSession ( w , r ) {
return
}
2019-01-20 05:02:52 +00:00
// Read file from URL, removing leading / as workdir is rel to us
2019-03-12 19:24:06 +00:00
consoleLog , e := ioutil . ReadFile ( r . URL . String ( ) [ 1 : ] )
2019-01-20 05:02:52 +00:00
if e != nil {
2019-03-12 19:24:06 +00:00
writeStr ( w , fmt . Sprintf ( "%s" , e ) )
2019-01-20 05:02:52 +00:00
return
}
lines := strings . Split ( string ( consoleLog ) , "\n" )
// Prevent log output from creating huge web pages.
tailL := 34
l := len ( lines ) - tailL
if l < 0 {
l = 0
}
consStat := lines [ 0 ]
fullConsLink := lines [ 1 ]
2019-01-25 06:09:17 +00:00
//jobTag := lines[2]
2019-01-20 05:02:52 +00:00
var tail [ ] string
var stat rune
2019-01-25 06:09:17 +00:00
var code int
2019-03-09 07:51:22 +00:00
n , _ := fmt . Sscanf ( consStat , "[%c %03d]" , & stat , & code )
2019-01-20 05:02:52 +00:00
_ = n
if l > 0 {
tail = lines [ len ( lines ) - tailL : ]
_ = fullConsLink
consoleLog = [ ] byte ( "<a href=\"" + fullConsLink + "\">full log</a>\n" + strings . Join ( tail , "\n" ) )
} else {
tail = lines [ 2 : ]
consoleLog = [ ] byte ( strings . Join ( tail , "\n" ) )
}
var codeColor string
var statWord string
if code != 0 {
codeColor = "finErrMarker"
statWord = fmt . Sprintf ( "E:%d" , code )
} else {
codeColor = "finOKMarker"
statWord = "Done"
}
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-01-20 05:02:52 +00:00
< html >
< head >
2019-01-27 21:20:40 +00:00
` +
2019-02-06 05:46:02 +00:00
favIconHTML ( ) +
2019-03-04 06:20:02 +00:00
refreshMetaTag ( stat , "5" ) +
2019-02-06 05:46:02 +00:00
compatJS ( ) +
consActiveSpinnerCSS ( ) +
consActiveSpinnerJS ( stat , codeColor , statWord ) +
2019-01-20 05:02:52 +00:00
`
< script >
window . onload = function ( ) {
appendSpinner ( ) ;
scrollDown ( ) ; //scrollTo(0,0);
}
< / script >
< / head >
< body >
` )
2019-03-12 19:24:06 +00:00
writeStr ( w , logoShortHdrHTML ( ) )
writeStr ( w , "<pre>" )
writeStr ( w , string ( consoleLog ) )
writeStr ( w , "\n</pre>" )
2019-01-20 05:02:52 +00:00
2019-03-12 19:24:06 +00:00
writeStr ( w , "<pre>" + fmt . Sprintln ( r . URL ) + "</pre>" )
writeStr ( w , `
2019-01-20 05:02:52 +00:00
< / body >
< / html >
` )
}
2019-02-08 05:32:45 +00:00
type jobCtx struct {
w http . ResponseWriter
mainCtx context . Context
jobTag string
jobOpts string
jobEnv [ ] string
}
2019-09-12 23:11:17 +00:00
type hookEvt struct {
Ref string ` json:"ref" `
Before string ` json:"before" `
After string ` json:"after" `
Compare_url string ` json:"compare_url" `
Commits [ ] struct {
Id string ` json:"id" `
Message string ` json:"message" `
Url string ` json:"url" `
Author struct {
Name string ` json:"name" `
Email string ` json:"email" `
Username string ` json:"username" `
}
}
}
2019-03-12 20:22:07 +00:00
// execJob spawns the actual job, waiting for it to complete and
// marks the runlog entry to indicate completion status and supply
// the artifact link.
//
//nolint:gocyclo
2019-09-12 23:11:17 +00:00
func execJob ( j jobCtx , hookData hookEvt ) {
2019-02-08 05:32:45 +00:00
// Some wrinkles in the exec.Command API: If there are no args,
// one must completely omit the args ... to avoid strange errors
// with some commands that see a blank "" arg and complain.
cmd := strings . Split ( cmdMap [ j . jobTag ] , " " ) [ 0 ]
cmdStrList := strings . Split ( cmdMap [ j . jobTag ] , " " ) [ 1 : ]
//fmt.Printf("%s %v\n", cmd, cmdStrList)
cmdCancelCtx , cmdCancelFunc := context . WithCancel ( j . mainCtx )
defer cmdCancelFunc ( )
2019-02-20 02:42:35 +00:00
2019-02-08 05:32:45 +00:00
var c * exec . Cmd
if len ( cmdStrList ) > 0 {
c = exec . CommandContext ( cmdCancelCtx , cmd , strings . Join ( cmdStrList , " " ) )
} else {
c = exec . CommandContext ( cmdCancelCtx , cmd )
}
var instColourIdx uint32
2019-03-09 07:51:22 +00:00
if indStyle == indStyleColour || indStyle == indStyleBoth {
2019-02-08 05:32:45 +00:00
instColourIdx = rand . Uint32 ( ) % uint32 ( len ( instColours ) )
2019-03-09 07:51:22 +00:00
instCounter ++
2019-02-08 05:32:45 +00:00
} else {
instColourIdx = 0
}
instColour := instColours [ instColourIdx ]
dirTmp , _ := filepath . Abs ( jobHomeDir )
workDir , terr := ioutil . TempDir ( dirTmp , fmt . Sprintf ( "bacillus_%s_%s_" , j . jobOpts , j . jobTag ) )
c . Dir = workDir
2019-03-29 06:30:39 +00:00
jobID := workDir [ strings . LastIndex ( workDir , "_" ) + 1 : ]
2019-02-08 05:32:45 +00:00
var indent int64
var indentStr string
2019-03-09 07:51:22 +00:00
if indStyle == indStyleIndent || indStyle == indStyleBoth {
2019-02-08 05:32:45 +00:00
indent , _ = strconv . ParseInt ( jobID , 10 , 64 )
indentStr = strings . Repeat ( "-" , int ( indent % 8 ) + 4 )
}
if terr != nil {
log . Printf ( "[ERROR creating workdir (%s) for job %s trigger.]\n" , terr , j . jobTag )
} else {
var workerOutputPath string
var workerOutputFile * os . File
consoleFName := "console.out"
workerOutputPath = workDir + "/" + consoleFName
2019-03-29 06:30:39 +00:00
workerOutputRelPath := fmt . Sprintf ( "%s/bacillus_%s_%s_%s/%s" ,
jobHomeDir ,
j . jobOpts ,
j . jobTag ,
jobID ,
consoleFName )
2019-02-08 05:32:45 +00:00
if attachStdout {
c . Stdout = os . Stdout
c . Stderr = os . Stderr
} else {
workerOutputFile , _ = os . Create ( workerOutputPath )
c . Stdout = workerOutputFile
c . Stderr = workerOutputFile
}
c . Env = append ( c . Env , fmt . Sprintf ( "USER=%s" , os . Getenv ( "USER" ) ) )
c . Env = append ( c . Env , fmt . Sprintf ( "HOME=%s" , os . Getenv ( "HOME" ) ) )
c . Env = append ( c . Env , fmt . Sprintf ( "BACILLUS_JOBID=%s" , jobID ) )
c . Env = append ( c . Env , fmt . Sprintf ( "BACILLUS_JOBTAG=%s" , j . jobTag ) )
c . Env = append ( c . Env , fmt . Sprintf ( "BACILLUS_WORKDIR=%s" , workDir ) )
c . Env = append ( c . Env , fmt . Sprintf ( "BACILLUS_ARTFDIR=%s" , fmt . Sprintf ( "%s/../../artifacts/bacillus_%s_%s_%s" , workDir , j . jobOpts , j . jobTag , jobID ) ) )
c . Env = append ( c . Env , j . jobEnv ... )
2019-09-12 23:11:17 +00:00
// Extra environment is provided by webhooks, so we'll set those
// separately if JSON is present.
// It is the job script's responsibility to check for these and,
// if not set, default sensibly (eg., if the push was done by a
// raw git post-receive hook rather than a webhook, there will be
// no BACILLUS_REF; script usually should default to "refs/master")
if hookData . Ref != "" {
2019-09-12 23:17:35 +00:00
c . Env = append ( c . Env , fmt . Sprintf ( "BACILLUS_REF=%s" , strings . TrimPrefix ( hookData . Ref , "heads/" ) ) )
2019-09-12 23:11:17 +00:00
}
if len ( hookData . Commits ) > 0 {
c . Env = append ( c . Env , fmt . Sprintf ( "BACILLUS_COMMITID=%s" , hookData . Commits [ 0 ] . Id ) )
}
2019-02-08 05:32:45 +00:00
// JOB STATUS METADATA PREPENDED TO console.out
// Job output status is encoded in first line of output log.
// [1 2]
// 1: state: r = running f = finished
// 2: completion status: <n> = exit status, 0 = success; else failure
// status uses UNIX shell exit status convention (base 10 0-255))
//
// Line 2 is the relative path of the console.log file itself, used to
// build a link to it for the /fullconsole/ endpoint link
//
// Line 3 is the JOBTAG of the job generating this console.out, used
// by the top "/" endpoint to show recently active jobs (ie., those with
// workdirs still present)
//
2019-03-29 06:30:39 +00:00
fmt . Fprintf ( c . Stdout , "[r 255]\n" ) //nolint:errcheck
fmt . Fprintf ( c . Stdout , "%s\n" ,
strings . Replace ( workerOutputRelPath , jobHomeDir , "/" + jobHomeDir + "/fullconsole" , 1 ) ) //nolint:errcheck
fmt . Fprintf ( c . Stdout , "%s\n" , j . jobTag ) //nolint:errcheck
2019-02-08 05:32:45 +00:00
cerr := c . Start ( )
if cerr != nil {
log . Printf ( "[exec.Cmd: %+v]\n" , c )
j . w . WriteHeader ( 500 )
2019-03-12 19:24:06 +00:00
writeStr ( j . w , "ERR" )
2019-02-08 05:32:45 +00:00
log . Printf ( "%s[ERROR on job %s trigger.]\n" , indentStr ,
j . jobTag )
} else {
2019-03-15 05:43:01 +00:00
if len ( runningJobs ) >= int ( runningJobsLimit ) {
writeStr ( j . w , "WHOA BESSIE" )
log . Printf ( "<!--JOBID:%s:JOBID--><span style='background-color:grey'><span style='background-color:maroon'>XXX</span>%s[%s not launched: running job limit reached]</span><!--COMPLETION-->\n" ,
jobID ,
"" , /*indentStr,*/
j . jobTag )
return
}
2019-03-12 19:24:06 +00:00
runningJobs [ jobID ] = & runningJobInfo {
jobCanceller : cmdCancelFunc , jobTag : j . jobTag , workDir : workDir }
2019-03-06 18:28:12 +00:00
2019-03-12 19:24:06 +00:00
writeStr ( j . w , "OK" )
2019-03-12 22:22:49 +00:00
log . Printf ( "<!--JOBID:%s:JOBID-->" +
2019-03-13 07:52:36 +00:00
"<span style='background-color:%s'><a style='display:inline;' href='%s' title='Running'>[∿]</a>%s[%s{%s}<a style='display:inline;' href='/cancel/?id=%s' title='Cancel'>[✗]</a> triggered.]<!--:STAGE:--></span>\n" ,
2019-02-08 05:32:45 +00:00
jobID , instColour ,
workerOutputRelPath ,
indentStr ,
j . jobTag , jobID ,
jobID )
}
werr := c . Wait ( )
if werr , ok := werr . ( * exec . ExitError ) ; ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
var exitStatus uint32
if status , ok := werr . Sys ( ) . ( syscall . WaitStatus ) ; ok {
exitStatus = uint32 ( status . ExitStatus ( ) )
// exec.Cmd automatically closes its files on exit, so we need to
// reopen here to write the status at offset 0
workerOutputFile , _ = os . OpenFile ( workerOutputPath , os . O_RDWR , 0777 )
2019-03-12 19:24:06 +00:00
fmt . Fprintf ( workerOutputFile , "[f %03d]" , int8 ( exitStatus ) ) //nolint:errcheck
2019-02-08 05:32:45 +00:00
//log.Print(c.Stderr /*stdErrBuffer*/)
//log.Printf("%s[Exit Status: %d]\n", indentStr, int32(exitStatus)) //#
}
} else {
// exec.Cmd automatically closes its files on exit, so we need to
// reopen here to write the status at offset 0
workerOutputFile , _ = os . OpenFile ( workerOutputPath , os . O_RDWR , 0777 )
2019-03-12 19:24:06 +00:00
fmt . Fprintf ( workerOutputFile , "[f %03d]" , 0 ) //nolint:errcheck
2019-02-08 05:32:45 +00:00
//workerOutputFile.WriteAt([]byte(fmt.Sprintf("[f %03d]", 0)), 0)
}
2019-03-13 07:52:36 +00:00
stageStr := ""
if showStagesOnFinished {
currentStage , e := ioutil . ReadFile ( runningJobs [ jobID ] . workDir + "/_stage" )
stageStr = string ( currentStage )
if e == nil {
stageStr = " |" +
2019-03-14 03:00:27 +00:00
strings . TrimSpace ( strings . Replace ( brevity . PreEllipse ( stageStr , ":" , 3 ) , ":" , " ∘ " , - 1 ) ) +
2019-03-13 07:52:36 +00:00
"|"
} else {
stageStr = "|???|"
}
}
2019-02-08 05:32:45 +00:00
if werr == nil {
2019-03-13 07:52:36 +00:00
log . Printf ( "<!--JOBID:%s:JOBID--><span style='background-color:%s'><a href='%s' title='Done'>[✓]</a>%s[%s{%s}<a href='/artifacts/bacillus_%s_%s_%s/' title='Artifacts'>[⩐]</a> completed with status 0]%s</span><!--COMPLETION-->\n" ,
2019-02-08 05:32:45 +00:00
jobID , instColour ,
workerOutputRelPath ,
indentStr ,
j . jobTag , jobID ,
2019-03-13 07:52:36 +00:00
j . jobOpts , j . jobTag , jobID ,
stageStr )
2019-02-08 05:32:45 +00:00
} else {
2019-03-13 07:52:36 +00:00
log . Printf ( "<!--JOBID:%s:JOBID--><span style='background-color:%s'><span style='background-color:red'><a href='%s' title='Done With Errors'>[!]</a></span>%s[%s{%s}<a href='/artifacts/bacillus_%s_%s_%s/' title='Partial Artifacts'>[⩌]</a> completed with error %s]%s</span><!--COMPLETION-->\n" ,
2019-02-08 05:32:45 +00:00
jobID , instColour ,
workerOutputRelPath ,
indentStr ,
j . jobTag , jobID ,
j . jobOpts , j . jobTag , jobID ,
2019-03-13 07:52:36 +00:00
werr ,
stageStr )
2019-02-08 05:32:45 +00:00
}
2019-03-13 07:52:36 +00:00
delete ( runningJobs , jobID )
2019-02-08 05:32:45 +00:00
}
}
func jobCancelHandler ( w http . ResponseWriter , r * http . Request ) {
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/html;charset=UTF-8" )
2019-02-08 05:32:45 +00:00
if ! httpAuthSession ( w , r ) {
return
}
2019-01-22 03:41:43 +00:00
2019-02-08 05:32:45 +00:00
v , ok := r . URL . Query ( ) [ "id" ]
jobID := "undefined"
if ok {
jobID = v [ 0 ]
}
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-02-08 05:32:45 +00:00
< html >
< head > ` +
favIconHTML ( ) +
2019-02-21 05:38:07 +00:00
goBackJS ( "1" , "3000" ) + `
2019-02-08 05:32:45 +00:00
< / head >
< body ` +bodyBgndHTMLAttribs()+ ` >
` )
2019-03-12 19:24:06 +00:00
if runningJobs [ jobID ] != nil && runningJobs [ jobID ] . jobCanceller != nil {
runningJobs [ jobID ] . jobCanceller ( )
writeStr ( w , fmt . Sprintf ( "<pre>Cancelled jobID %s</pre>\n" , jobID ) )
2019-02-08 05:32:45 +00:00
} else {
2019-03-12 19:24:06 +00:00
writeStr ( w , fmt . Sprintf ( "<pre>jobID %s already done or not found.</pre>\n" , jobID ) )
2019-02-08 05:32:45 +00:00
}
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-02-08 05:32:45 +00:00
< / body >
< / html > ` )
}
2019-03-12 19:24:06 +00:00
// Launch a job listener endpoint, which in the case of simple jobs
// directly calls the job, or, in the case of parameterized jobs,
// dynamically builds and emits a page containing an HTML form with which
// the user may set job parameters before submitting to launch the job.
//
// The HTML emitted is selected by whether the visiting URL has
2019-09-12 23:11:17 +00:00
// no parameters, ?param or ?usingParams: no parameters directly launches a job,
// ?param emits an HTML form page, and ?usingParams launches the job with the
2019-03-12 19:24:06 +00:00
// submitted parameters from the ?param form page.
2019-02-21 05:38:07 +00:00
func launchJobListener ( mainCtx context . Context , cmd , jobTag , jobOpts string , jobEnv [ ] string , cmdMap map [ string ] string ) {
2019-02-22 06:22:36 +00:00
origJobEnv := jobEnv // saved to reset the env on each invocation
2019-01-31 03:07:51 +00:00
http . HandleFunc ( fmt . Sprintf ( "/%s" , jobTag ) ,
2019-01-20 05:02:52 +00:00
func ( w http . ResponseWriter , r * http . Request ) {
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/html;charset=UTF-8" )
2019-02-22 06:22:36 +00:00
2019-02-21 05:38:07 +00:00
jobEnv = origJobEnv // reset each time invoked, we append to it
2019-01-31 07:47:09 +00:00
if ! httpAuthSession ( w , r ) {
return
}
2019-02-22 06:34:00 +00:00
2019-09-12 23:11:17 +00:00
// Check if JSON is present (from a webhook)
decoder := json . NewDecoder ( r . Body )
var hookData = hookEvt { }
jsonErr := decoder . Decode ( & hookData )
_ = jsonErr
2019-03-12 19:24:06 +00:00
// Depending on whether the page being emitted is ?param (form)
2019-09-12 23:11:17 +00:00
// or ?usingParams (form submission/job launch), set how many
2019-03-12 19:24:06 +00:00
// pages the launch confirmation page needs to jump back
// to return to the dashboard or runlog page.
2019-02-22 06:22:36 +00:00
var pagesBack string
2019-09-12 23:11:17 +00:00
_ , ok := r . URL . Query ( ) [ "usingParams" ]
2019-02-21 05:38:07 +00:00
if ok {
2019-02-22 08:17:51 +00:00
pagesBack = "2"
2019-02-22 06:22:36 +00:00
} else {
2019-02-22 08:17:51 +00:00
pagesBack = "1"
2019-02-21 05:38:07 +00:00
}
2019-02-22 06:34:00 +00:00
2019-02-22 06:22:36 +00:00
headerFragS := "<html><head>" + favIconHTML ( ) + logoShortHdrHTML ( )
headerFragM := goBackJS ( pagesBack , "3000" )
headerFragE := "</head>"
bodyFragB := "<body " + bodyBgndHTMLAttribs ( ) + ">"
bodyFragM := ""
bodyFragE := "</body></html>"
_ , ok = r . URL . Query ( ) [ "param" ]
2019-02-21 05:38:07 +00:00
if ok {
2019-02-22 06:22:36 +00:00
headerFragM = ""
}
2019-03-12 19:24:06 +00:00
writeStr ( w , headerFragS + headerFragM + headerFragE )
writeStr ( w , bodyFragB )
2019-02-22 06:22:36 +00:00
if shutdownModeActive {
bodyFragM = fmt . Sprintf ( "<pre>Server is in shutdown mode, come back later.</pre>\n" )
2019-02-22 08:17:51 +00:00
bodyFragM += goBackJS ( pagesBack , "3000" )
2019-02-22 06:22:36 +00:00
} else if _ , ok := r . URL . Query ( ) [ "param" ] ; ok {
2019-03-12 19:24:06 +00:00
// Get job-defined parameter form
2019-02-22 06:22:36 +00:00
bodyFragM = genParameterizedBuildForm ( jobTag , cmd )
2019-09-12 23:11:17 +00:00
} else if _ , ok = r . URL . Query ( ) [ "usingParams" ] ; ok {
// If we're called back with ?usingParams, which is submitted
2019-03-12 19:24:06 +00:00
// form data from dynamically-generated ?param form above,
2019-02-22 06:22:36 +00:00
// parse those values from r.URL.Query(), adding to jobEnv[].
2019-03-12 19:24:06 +00:00
r . ParseForm ( ) //nolint:errcheck
2019-02-21 05:38:07 +00:00
for k , v := range r . Form {
if len ( v ) > 0 {
jobEnv = append ( jobEnv , k + ` =" ` + v [ 0 ] + ` " ` )
}
}
2019-02-22 06:22:36 +00:00
bodyFragM = fmt . Sprintf ( "<pre>Triggered parameterized build %s</pre>\n" , jobTag )
2019-03-12 19:24:06 +00:00
// Launch parameterized job
2019-09-12 23:11:17 +00:00
go execJob ( jobCtx { w , mainCtx , jobTag , jobOpts , jobEnv } , hookEvt { } )
2019-03-13 07:52:36 +00:00
//!A writeStr(w, `<audio id='jobRunSound' type='audio/mpeg' src='audio/13280__schademans__pipe1.mp3'></audio>`)
//!A writeStr(w, `<script>document.getElementById("jobRunSound").play();</script>`)
2019-02-22 06:22:36 +00:00
} else {
2019-03-12 19:24:06 +00:00
// Launch simple job
2019-02-22 06:22:36 +00:00
bodyFragM = fmt . Sprintf ( "<pre>Triggered %s</pre>\n" , jobTag )
2019-09-12 23:11:17 +00:00
go execJob ( jobCtx { w , mainCtx , jobTag , jobOpts , jobEnv } , hookData )
2019-03-13 07:52:36 +00:00
//!A writeStr(w, `<audio id='jobRunSound' type='audio/mpeg' src='audio/13280__schademans__pipe1.mp3'></audio>`)
//!A writeStr(w, `<script>document.getElementById("jobRunSound").play();</script>`)
2019-01-29 08:42:04 +00:00
}
2019-03-12 19:24:06 +00:00
fmt . Fprintf ( w , bodyFragM + bodyFragE ) // nolint:errcheck
2019-01-20 05:02:52 +00:00
} )
}
2019-03-12 19:24:06 +00:00
// rootPageHandler serves the 'root' ('main' or 'dashboard') page.
2019-01-26 02:59:18 +00:00
func rootPageHandler ( w http . ResponseWriter , r * http . Request ) {
2019-02-02 03:54:48 +00:00
// See if there are actions (currently just logout)
_ , ok := r . URL . Query ( ) [ "logout" ]
if ok {
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/html;charset=UTF-8" )
2019-02-02 03:54:48 +00:00
w . Header ( ) . Set ( "WWW-Authenticate" , ` Basic realm="Bacillus" ` )
w . WriteHeader ( http . StatusUnauthorized )
2019-03-12 19:24:06 +00:00
//writeStr(w, `<head><meta http-equiv="refresh" content="0;URL='/'" /></head>`)
writeStr ( w , ` <pre><a href='/'>You must log in.</a></pre> ` )
2019-02-02 03:54:48 +00:00
return
}
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/html;charset=UTF-8" )
2019-01-31 07:47:09 +00:00
if ! httpAuthSession ( w , r ) {
return
}
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-01-26 02:59:18 +00:00
< html >
2019-01-27 21:20:40 +00:00
< head > ` +
2019-02-06 05:46:02 +00:00
favIconHTML ( ) +
2019-03-04 06:20:02 +00:00
forceReloadOnHistJS ( ) +
/*refreshMetaTag('r', "10")+*/
2019-03-06 18:54:24 +00:00
xmlHTTPRequester ( "xhrLiveRunLogUpdate" , "/api/lru?tl=6" , ` document.getElementById('liveRunLog').innerHTML = xhttp.response; ` ) +
2019-02-06 05:46:02 +00:00
xmlHTTPRequester ( "xhrRunningJobsCount" , "/api/rjc" , ` document.getElementById('liveRunLogCount').innerHTML = xhttp.response; ` ) +
xhrlinkCSSFrag ( ) + `
2019-01-26 02:59:18 +00:00
< / head >
2019-02-06 05:46:02 +00:00
< body ` +bodyBgndHTMLAttribs()+ ` >
2019-01-26 02:59:18 +00:00
` )
2019-03-12 19:24:06 +00:00
writeStr ( w , logoHdrHTML ( ) )
2019-03-13 07:52:36 +00:00
//!A writeStr(w, "<audio id='jobRunSound' type='audio/mpeg' src='audio/13280__schademans__pipe1.mp3'></audio>")
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-01-26 02:59:18 +00:00
< pre >
2019-02-02 05:36:57 +00:00
< a href = ' / runlog ' > / runlog < / a > : main log / activity view
< a href = ' / artifacts ' > / artifacts < / a > : where jobs ( should ) leave their stuff
2019-01-26 02:59:18 +00:00
2019-03-15 05:43:01 +00:00
Latest Job Activity ( Running jobs : < span id = ' liveRunLogCount ' > ` +fmt.Sprintf("%d", len(runningJobs))+ ` < / span > Max ` +fmt.Sprintf("%d", runningJobsLimit)+ ` )
2019-02-02 07:50:05 +00:00
...
2019-03-06 18:54:24 +00:00
< span id = ' liveRunLog ' > ` +liveRunLogHTML(6)+ ` < / span >
2019-03-07 01:52:18 +00:00
2019-01-26 02:59:18 +00:00
LEGEND
[ & rtrif ; ] Start a job manually
2019-02-22 06:34:00 +00:00
[ & rtri ; ] Start a job with parameters
2019-01-26 02:59:18 +00:00
[ & cross ; ] Cancel a running job
[ & ccupssm ; ] View completed job artifacts
[ & ccups ; ] View partial artifacts for a failed job
2019-03-06 18:28:12 +00:00
[ < img style = ' border : none ; border - width : 0 px ; width : 0.8 em ; margin : 0 px ; padding : 0 px ; ' src = ' images / run - throbber . gif ' / > ] Job is running - click to view
2019-02-02 05:36:57 +00:00
[ & check ; ] Job completed with OK ( 0 ) status - click to view
< span style = ' background - color : red ' > [ ! ] < / span > Job completed with nonzero status - click to view
2019-01-26 02:59:18 +00:00
2019-03-15 07:47:12 +00:00
. . that ' s < a class = "xhrlink" style = "text-decoration:none" href = "/about" > about < / a > it .
2019-01-29 08:42:04 +00:00
Oh , and in case you need to ...
2019-02-22 08:10:45 +00:00
< a href = ' / shutdown ' > prevent any new jobs for a graceful shutdown < / a > ( afterwards , use < strong > / rudeshutdown < / strong > )
2019-01-29 08:42:04 +00:00
< a href = ' / cancelshutdown ' > cancel a planned shutdown < / a >
2019-02-08 07:03:31 +00:00
` )
if basicAuth {
2019-03-12 19:24:06 +00:00
writeStr ( w , ` <a href=' ` + logoutURI + ` ' > logout < / a >
2019-02-08 07:03:31 +00:00
` )
}
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-08-29 03:26:41 +00:00
Jobs Served ( click Play to manually trigger ) ` +
manualJobTriggersHTML ( false ) +
sayingFooterHTML ( ) )
2019-02-03 06:45:45 +00:00
2019-03-12 19:24:06 +00:00
writeStr ( w , manualJobTriggersJS ( ) )
writeStr ( w , `
2019-01-26 02:59:18 +00:00
< / body >
< / html >
` )
}
2019-03-12 19:24:06 +00:00
// Perform a logout from HTTP Basic Auth
//
// Apparently it is quite difficult to clean out HTTP basic auth in modern
// browsers.
//
2019-02-02 05:36:57 +00:00
// This hack is from https://stackoverflow.com/a/14329930/1012159
var logoutURI = ` javascript:(function(c) { var a,b="Logged out.";try { a=document.execCommand("ClearAuthenticationCache")}catch(d) { }a||((a=window.XMLHttpRequest?new window.XMLHttpRequest:window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):void 0)?(a.open("HEAD",c||location.href,!0,"logout",(new Date).getTime().toString()),a.send(""),a=1):a=void 0);a||(b="Your browser is too old or too weird to support log out functionality. Close all windows and restart the browser.");alert(b)})(/*pass safeLocation here if you need*/); `
2019-02-08 05:32:45 +00:00
2019-02-02 05:36:57 +00:00
//var logoutURI = `/?logout`
2019-03-12 19:24:06 +00:00
// cancelShutdownHandler .. does what you'd expect, cancels a planned
// server shutdown.
//
// NOTE the server does not itself shutdown after scheduling one without
// explicit admin action, by killing the process or manually visiting
// the /rudeshutdown URI endpoint.
2019-01-29 08:42:04 +00:00
func cancelShutdownHandler ( w http . ResponseWriter , r * http . Request ) {
2019-02-08 05:32:45 +00:00
//fmt.Println(r.URL)
2019-01-29 08:42:04 +00:00
shutdownModeActive = false
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/html;charset=UTF-8" )
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-01-29 08:42:04 +00:00
< html >
< head > ` +
2019-02-06 05:46:02 +00:00
favIconHTML ( ) +
2019-02-21 05:38:07 +00:00
goBackJS ( "1" , "3000" ) + `
2019-01-29 08:42:04 +00:00
< / head >
2019-02-06 05:46:02 +00:00
< body ` +bodyBgndHTMLAttribs()+ ` >
2019-01-29 08:42:04 +00:00
` )
2019-03-15 09:54:15 +00:00
if demoMode {
writeStr ( w , fmt . Sprintf ( "<pre>Shutdown mode disabled by admin.</pre>\n" ) )
} else {
writeStr ( w , fmt . Sprintf ( "<pre>Shutdown mode off.</pre>\n" ) )
}
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-01-29 08:42:04 +00:00
< / body >
< / html > ` )
}
2019-03-12 19:24:06 +00:00
// shutdownHandler puts the server into shutdown mode: refuse to start
// any new jobs until the /cancelshutdown endpoint is visited, or
// the admin kills the server or visits /rudeshutdown to tell it to exit.
2019-01-29 08:42:04 +00:00
func shutdownHandler ( w http . ResponseWriter , r * http . Request ) {
2019-02-08 05:32:45 +00:00
//fmt.Println(r.URL)
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/html;charset=UTF-8" )
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-01-29 08:42:04 +00:00
< html >
< head > ` +
2019-02-06 05:46:02 +00:00
favIconHTML ( ) +
2019-02-21 05:38:07 +00:00
goBackJS ( "1" , "3000" ) + `
2019-01-29 08:42:04 +00:00
< / head >
2019-02-06 05:46:02 +00:00
< body ` +bodyBgndHTMLAttribs()+ ` >
2019-01-29 08:42:04 +00:00
` )
2019-03-15 09:54:15 +00:00
if demoMode {
writeStr ( w , fmt . Sprintf ( "<pre>Shutdown mode disabled by admin.</pre>\n" ) )
} else {
shutdownModeActive = true
writeStr ( w , fmt . Sprintf ( "<pre>Shutdown mode on. No new jobs can start.</pre>\n" ) )
}
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-01-29 08:42:04 +00:00
< / body >
< / html > ` )
}
2019-03-12 19:24:06 +00:00
// aboutPageHandler displays author/license information.
2019-03-08 06:23:57 +00:00
func aboutPageHandler ( w http . ResponseWriter , r * http . Request ) {
if ! httpAuthSession ( w , r ) {
return
}
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-03-08 06:23:57 +00:00
< html >
< head > ` +
favIconHTML ( ) +
2019-03-12 19:24:06 +00:00
//xhrlinkCSSFrag()+
//xmlHTTPRequester("xhrLiveRunLogUpdate", fmt.Sprintf("/api/lru?tl=%d", runLogTailLines), `document.getElementById('liveRunLog').innerHTML = xhttp.response;`)+
2019-03-08 06:23:57 +00:00
logoShortHdrHTML ( ) + `
< / head >
< body ` +bodyBgndHTMLAttribs()+ ` > ` )
2019-03-12 19:24:06 +00:00
writeStr ( w , ` <p><img src="images/BenderCI.jpg" width="600"/></p> ` )
writeStr ( w , goBackJS ( "1" , "10000" ) )
writeStr ( w , ` < pre >
2019-03-08 06:23:57 +00:00
bacill & mu ; s CI server . Written in < a href = "https://golang.org/" > Go < / a >
& copy ; Copyright 2019 by Russ Magee . All Rights Reserved .
< / pre >
< / body >
< / html >
` )
}
2019-03-12 19:24:06 +00:00
// rudeShutdownHandler tells the server to exit. The /shutdown endpoint
// should be visited and, if possible, running jobs be allowed to finish
// before using this endpoint.
2019-01-29 08:42:04 +00:00
func rudeShutdownHandler ( w http . ResponseWriter , r * http . Request ) {
2019-02-08 05:32:45 +00:00
//fmt.Println(r.URL)
2019-07-12 05:08:06 +00:00
w . Header ( ) . Set ( "Content-type" , "text/html;charset=UTF-8" )
2019-03-12 19:24:06 +00:00
writeStr ( w , `
2019-01-29 08:42:04 +00:00
< html >
< head > ` +
2019-02-06 05:46:02 +00:00
favIconHTML ( ) + `
2019-01-29 08:42:04 +00:00
< / head >
2019-02-06 05:46:02 +00:00
< body ` +bodyBgndHTMLAttribs()+ ` >
2019-01-29 08:42:04 +00:00
` )
2019-03-15 09:54:15 +00:00
if demoMode {
writeStr ( w , fmt . Sprintf ( "<pre>.. rudeshutdown disabled by admin.</pre>\n" ) )
writeStr ( w , `
2019-01-29 08:42:04 +00:00
< / body >
< / html > ` )
2019-03-15 09:54:15 +00:00
} else {
writeStr ( w , fmt . Sprintf ( "<pre>.. so cold... so very, very cold..</pre>\n" ) )
writeStr ( w , `
< / body >
< / html > ` )
killSwitch <- true
}
2019-01-29 08:42:04 +00:00
}
2019-03-12 20:22:07 +00:00
// patchLiveRunEntries looks at a limited back-history of runlog entries
// and updates ones representing currently-running jobs with live status.
//
// HTML comment blocks inside elements, <!--JOBID:>...<:JOBID--> and <!--:STAGE:-->
// are used to find elements to patch with live run status info.
// Recently-completed jobs also have their in-progress symbol at the start
// replaced if the placeholder comment <!--COMPLETION--> is found in a
// later log entry.
//
//nolint:gocyclo
func patchLiveRunEntries ( idx , horizon int , fixed [ ] string ) [ ] string {
if strings . Count ( fixed [ idx ] , "<!--COMPLETION-->" ) != 0 {
// Found a completed job. Seek a few entries back
// to mark the job launch stmt, hiding the in-progress
// and cancel links within.
var jidStart , jidEnd int
var jobID string
jidStart = strings . Index ( fixed [ idx ] , "<!--JOBID:" )
if jidStart != - 1 {
jidStart += len ( "<!--JOBID:" )
jidEnd = strings . Index ( fixed [ idx ] , ":JOBID-->" )
}
if jidStart != - 1 && jidEnd != - 1 {
jobID = fixed [ idx ] [ jidStart : jidEnd ]
jobTag := "<!--JOBID:" + jobID + ":JOBID-->"
for seekIdx := idx - 1 ; seekIdx >= 0 && seekIdx > horizon ; seekIdx -- {
// NOTE we're modifying the 'live' view of
// the logfile, not the direct data on disk, so
// no need to replace byte-for-byte.
// (If this func is optimized to be zero-copy
// however, it might need to be.)
if strings . Contains ( fixed [ seekIdx ] , jobTag ) {
fixed [ seekIdx ] = strings . Replace ( fixed [ seekIdx ] ,
"display:inline" , "display:none" , - 1 )
if indStyle == indStyleBoth || indStyle == indStyleIndent {
fixed [ seekIdx ] = strings . Replace ( fixed [ seekIdx ] ,
"---" , "------" , 1 )
} else if indStyle == "colour" {
fixed [ seekIdx ] = strings . Replace ( fixed [ seekIdx ] ,
"[job" , " [job" , 1 )
}
}
}
}
} else if strings . Contains ( fixed [ idx ] , "<!--:STAGE:-->" ) &&
strings . Contains ( fixed [ idx ] , "[∿]" ) {
fixed [ idx ] = strings . Replace ( fixed [ idx ] , "∿" , "<img style='border:none; border-width:0px; width:0.8em; margin:0px; padding:0px;' src='images/run-throbber.gif'/>" , 1 )
// Found an fixed[idx] for a running job;
// fetch the stage, if defined, and add it to the
// live line's view.
var jidStart , jidEnd int
var jobID string
jidStart = strings . Index ( fixed [ idx ] , "<!--JOBID:" )
if jidStart != - 1 {
jidStart += len ( "<!--JOBID:" )
jidEnd = strings . Index ( fixed [ idx ] , ":JOBID-->" )
}
if jidStart != - 1 && jidEnd != - 1 {
jobID = fixed [ idx ] [ jidStart : jidEnd ]
}
if runningJobs [ jobID ] != nil {
currentStage , e := ioutil . ReadFile ( runningJobs [ jobID ] . workDir + "/_stage" )
if e == nil {
2019-03-14 03:00:27 +00:00
stageStr := brevity . PreEllipse ( string ( currentStage ) , ":" , 3 )
2019-03-12 20:22:07 +00:00
fixed [ idx ] = strings . Replace ( fixed [ idx ] , "<!--:STAGE:-->" ,
2019-03-13 07:52:36 +00:00
" |<strong>" +
strings . TrimSpace ( strings . Replace ( stageStr , ":" , " ∘ " , - 1 ) ) +
2019-03-12 20:22:07 +00:00
"<img style='border:none; border-width:0px; width:0.8em; margin:0px; padding:0px; padding-left:1px;' src='images/stage-throbber.gif'/>" +
2019-03-13 07:52:36 +00:00
"</strong>|" , 1 )
2019-03-12 20:22:07 +00:00
}
}
}
return fixed
}
func patchLiveViewOfRunLogEntries ( orig [ ] string , horizon int ) ( fixed [ ] string ) {
2019-03-12 19:24:06 +00:00
//FIXME: There is definitely a less copy-intensive way to do this.
//
2019-02-06 05:46:02 +00:00
// The data being patched is a 'live' view, limited to a tail
// portion of the actual run.log. In light of this we only need
// to reconcile finished jobs back a short distance, larger than
// the displayed tail length, in lines.
// Jobs should take longer than a few seconds, which is the
// refresh interval of the /runlog endpoint; so there *should*
// only be a very small number of entries that have completed
// since the last scan and still visible on the 'live' web view.
// We'll only look for those few, so if there was a spamming run of
// short-lived jobs, we might not mark all of them as completed.
// Meh. Not worth an O(n^2) operation.
fixed = orig
// As described above, prevent excessive processing for live web view
if horizon > 255 {
horizon = 255
}
l := len ( fixed ) - 1
if l > 1 {
if l > horizon {
horizon = l - horizon
} else {
horizon = 0
}
for idx := l ; idx > horizon ; idx -- {
2019-03-12 20:22:07 +00:00
fixed = patchLiveRunEntries ( idx , horizon , fixed )
2019-02-06 05:46:02 +00:00
}
}
return fixed
}
2019-01-20 05:02:52 +00:00
func main ( ) {
2019-01-25 06:09:17 +00:00
var createRunlog bool
2019-01-20 05:02:52 +00:00
flag . StringVar ( & addrPort , "a" , ":9990" , "[addr]:port on which to listen" )
2019-01-31 07:47:09 +00:00
flag . BoolVar ( & basicAuth , "auth" , true , "enable basic http auth login (be sure to also set -u and -p)" )
flag . StringVar ( & strUser , "u" , httpAuthUser , "web UI and endpoint username" )
flag . StringVar ( & strPasswd , "p" , httpAuthPasswd , "web UI and endpoint password" )
2019-03-29 06:30:39 +00:00
flag . StringVar ( & jobHomeDir , "w" , "workdir" , "workdir for jobs (relative to bacillus launch dir)" )
2019-01-25 06:09:17 +00:00
flag . BoolVar ( & createRunlog , "c" , false , "set true/1 to create new run.log, overwriting old one" )
2019-03-09 07:51:22 +00:00
flag . StringVar ( & indStyle , "i" , indStyleBoth , "job entry indicator style [none|indent|colour|both]" )
2019-01-24 05:42:02 +00:00
flag . IntVar ( & runLogTailLines , "rl" , 30 , "Scroll length of runlog (set to 0 for no limit)" )
2019-03-15 05:43:01 +00:00
flag . UintVar ( & runningJobsLimit , "jl" , 8 , "Max. concurrently running jobs" )
2019-01-20 05:02:52 +00:00
flag . BoolVar ( & attachStdout , "s" , false , "set to true to see worker stdout/err if running in terminal" )
2019-03-13 07:52:36 +00:00
flag . BoolVar ( & showStagesOnFinished , "F" , false , "set to true to show stages on finished jobs in runlog" )
2019-03-15 09:54:15 +00:00
flag . BoolVar ( & demoMode , "D" , false , "set true/1 to enable public demo mode -- users cannot /shutdown or /rudeshutdown" )
2019-01-20 05:02:52 +00:00
flag . Parse ( )
2019-09-06 02:41:29 +00:00
killSwitch = make ( chan bool , 1 ) // ensure a single send can proceed unblocked
2019-01-20 05:02:52 +00:00
mainCtx := context . Background ( )
2019-01-31 03:07:51 +00:00
cmdMap = make ( map [ string ] string )
2019-03-12 19:24:06 +00:00
runningJobs = make ( map [ string ] * runningJobInfo )
2019-01-20 05:02:52 +00:00
2019-01-25 06:09:17 +00:00
var logfile * os . File
var cerr error
runLogFileName := fmt . Sprintf ( "run%s.log" , strings . Split ( addrPort , ":" ) [ 1 ] )
if ! createRunlog {
logfile , cerr = os . OpenFile ( runLogFileName , os . O_RDWR , 0644 )
}
if cerr != nil || createRunlog {
logfile , _ = os . Create ( runLogFileName )
}
2019-01-24 05:42:02 +00:00
2019-01-20 05:02:52 +00:00
log . SetOutput ( logfile )
2019-09-06 02:41:29 +00:00
log . Printf ( "[bacillus %s startup]\n" , version )
2019-01-31 03:07:51 +00:00
log . Printf ( "[listening on %s]\n" , addrPort )
2019-01-20 05:02:52 +00:00
2019-01-22 10:07:04 +00:00
//log.Printf("Registering handler for /runlog page.\n")
2019-02-02 05:36:57 +00:00
http . HandleFunc ( "/runlog" , runLogHandler )
2019-03-29 06:30:39 +00:00
2019-01-20 05:02:52 +00:00
// Each non-switch argument is taken to be an endpoint (job) descriptor
// Syntax of an endpoint:
2019-01-23 22:37:58 +00:00
// endpoint:jobOpts:EVAR1=val1,EVAR2=val2[,...,EVAR<n>=val<n>]:cmd
2019-01-20 05:02:52 +00:00
for _ , e := range flag . Args ( ) {
2019-01-23 22:37:58 +00:00
2019-01-20 05:02:52 +00:00
fields := strings . Split ( e , ":" )
2019-01-23 22:37:58 +00:00
var tag string
var jobOpts string
2019-01-20 05:02:52 +00:00
var jobEnv [ ] string
var cmd string
2019-01-23 22:37:58 +00:00
if fields [ 0 ] != e {
2019-02-03 06:45:45 +00:00
// We use _ as field separator for jobOpts, jobID in workdir/ and
// artifacts/ dirs & job vars so they aren't allowed in the jobTag
2019-02-02 08:27:45 +00:00
tag = strings . Replace ( fields [ 0 ] , "_" , "-" , - 1 )
2019-01-23 22:37:58 +00:00
if len ( fields ) > 1 && len ( fields ) != 4 {
errStr := fmt . Sprintf ( "\n [%s]\n" +
" All endpoint specs must have exactly 4 fields:\n" +
" endpoint:jobOpts:envVars:cmd\n" +
" (jobOpts and envVars may be empty.)\n" ,
2019-01-24 05:42:02 +00:00
fields [ 0 ] )
2019-01-23 22:37:58 +00:00
fmt . Print ( errStr )
log . Fatal ( errStr )
}
jobOpts = fields [ 1 ]
_ = jobOpts
jobEnv = strings . Split ( fields [ 2 ] , "," )
cmd = fields [ 3 ]
cmdMap [ tag ] = cmd
// Launch webhook listeners for each defined endpoint
2019-01-31 03:07:51 +00:00
// Note presently only 'blind' hooks are supported
2019-01-23 22:37:58 +00:00
// (ie., if webhook request contains POST JSON data,
// it isn't read).
2019-02-21 05:38:07 +00:00
launchJobListener ( mainCtx , cmd , tag , jobOpts , jobEnv , cmdMap )
2019-01-20 05:02:52 +00:00
}
}
2019-01-24 05:42:02 +00:00
2019-01-20 05:02:52 +00:00
log . Printf ( "--BACILLUS READY--\n" )
2019-01-28 00:26:36 +00:00
// Seek to end in case we're reusing this runlog to preserve previous
// entries (yeah it's cheesy and probably error-prone if server was
2019-01-28 04:57:36 +00:00
// killed during running jobs. Big deal, those entries
// wouldn't show completion anyhow).
2019-03-12 19:24:06 +00:00
_ , _ = logfile . Seek ( 0 , 2 )
2019-01-20 05:02:52 +00:00
2019-01-22 03:41:43 +00:00
// Make a filesystem available for dir/file storage & retrieval by
// jobs and devs. Jobs are responsible for its proper use.
artifactBaseDir , aerr := filepath . Abs ( "artifacts" )
2019-01-27 21:20:40 +00:00
_ = artifactBaseDir
2019-01-22 03:41:43 +00:00
if aerr == nil {
2019-01-27 22:53:55 +00:00
http . Handle ( "/artifacts/" ,
http . StripPrefix ( "/artifacts/" ,
FileServer { Root : "/artifacts" ,
2019-01-28 04:57:36 +00:00
Handler : http . FileServer ( http . Dir ( "artifacts" ) ) } ) )
2019-01-22 03:41:43 +00:00
}
2019-01-24 05:42:02 +00:00
2019-01-24 00:06:07 +00:00
http . Handle ( "/images/" ,
2019-01-24 05:42:02 +00:00
http . StripPrefix ( "/images/" , http . FileServer ( http . Dir ( "images" ) ) ) )
2019-03-13 07:52:36 +00:00
//!A http.Handle("/audio/",
//!A http.StripPrefix("/audio/", http.FileServer(http.Dir("audio"))))
2019-01-21 05:55:04 +00:00
// Live runlog is just the tail of full runlog
2019-01-21 20:24:11 +00:00
http . HandleFunc ( "/fullrunlog/" , fullRunlogHandler )
2019-01-21 05:55:04 +00:00
2019-02-08 05:32:45 +00:00
// Endpoint to cancel a job
http . HandleFunc ( "/cancel/" , jobCancelHandler )
2019-01-20 05:02:52 +00:00
// A single endpoint handles the 'live' job output
http . HandleFunc ( "/" + jobHomeDir + "/" , consoleHandler )
2019-01-29 08:42:04 +00:00
2019-01-20 05:02:52 +00:00
// Similarly, a single endpoint handles static full job output
http . HandleFunc ( "/" + jobHomeDir + "/fullconsole/" , fullConsoleHandler )
2019-01-24 05:42:02 +00:00
2019-01-29 08:42:04 +00:00
// Enter shutdown mode (stop launching new jobs)
http . HandleFunc ( "/shutdown" , shutdownHandler )
// Enter shutdown mode (stop launching new jobs)
http . HandleFunc ( "/cancelshutdown" , cancelShutdownHandler )
// Rude exit (regardless of running jobs)
http . HandleFunc ( "/rudeshutdown" , rudeShutdownHandler )
2019-03-08 06:23:57 +00:00
// About page
http . HandleFunc ( "/about" , aboutPageHandler )
2019-02-06 05:46:02 +00:00
// Endpoint for XHR live run log updates
http . HandleFunc ( "/api/lru" , xhrLiveRunLogHandler )
// Endpoint for XHR live run log updates
http . HandleFunc ( "/api/rjc" , xhrRunningJobsCountHandler )
2019-02-02 03:54:48 +00:00
//// Logout sequence page
//http.HandleFunc("/logout", logoutPageHandler)
2019-02-01 17:07:43 +00:00
2019-01-22 08:53:00 +00:00
// And finally, the root fallback to give help on defined endpoints.
2019-01-26 02:59:18 +00:00
http . HandleFunc ( "/" , rootPageHandler )
2019-01-22 08:53:00 +00:00
2019-01-22 03:41:43 +00:00
//go func() {
// log.Fatal(http.ListenAndServe(":9991", http.FileServer(http.Dir(jobHomeDir))))
//}()
2019-01-29 08:42:04 +00:00
// Rather than use http.ListenAndServe() we break out that func
// and retain the http.Server var so we can call Shutdown() or
// Close() if needed.
server = & http . Server { Addr : addrPort , Handler : nil }
go func ( ) {
log . Fatal ( server . ListenAndServe ( ) )
} ( )
// .. and wait for a rude shutdown if requested
2019-03-12 19:24:06 +00:00
<- killSwitch
_ = server . Shutdown ( mainCtx )
2019-01-20 05:02:52 +00:00
}