2015-09-13 20:25:26 +00:00
///
2019-02-15 17:05:44 +00:00
/// Copyright 2015-2019 Oliver Giles
2015-09-13 20:25:26 +00:00
///
/// This file is part of Laminar
///
/// Laminar is free software: you can redistribute it and/or modify
/// it under the terms of the GNU General Public License as published by
/// the Free Software Foundation, either version 3 of the License, or
/// (at your option) any later version.
///
/// Laminar is distributed in the hope that it will be useful,
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/// GNU General Public License for more details.
///
/// You should have received a copy of the GNU General Public License
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
///
# include "laminar.h"
# include "server.h"
2015-09-19 12:36:03 +00:00
# include "conf.h"
2015-12-06 11:36:12 +00:00
# include "log.h"
2019-10-05 17:06:35 +00:00
# include "http.h"
# include "rpc.h"
2015-09-13 20:25:26 +00:00
# include <sys/wait.h>
2018-07-06 10:18:04 +00:00
# include <sys/mman.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <fcntl.h>
2015-09-19 15:24:20 +00:00
# include <fstream>
2015-12-06 12:47:43 +00:00
# include <zlib.h>
2015-09-13 20:25:26 +00:00
2017-07-31 05:56:58 +00:00
# define COMPRESS_LOG_MIN_SIZE 1024
2015-09-13 20:25:26 +00:00
# include <rapidjson/stringbuffer.h>
# include <rapidjson/writer.h>
// rapidjson::Writer with a StringBuffer is used a lot in Laminar for
2019-10-05 17:06:35 +00:00
// preparing JSON messages to send to HTTP clients. A small wrapper
2015-09-13 20:25:26 +00:00
// class here reduces verbosity later for this common use case.
class Json : public rapidjson : : Writer < rapidjson : : StringBuffer > {
public :
Json ( ) : rapidjson : : Writer < rapidjson : : StringBuffer > ( buf ) { StartObject ( ) ; }
template < typename T >
2018-01-04 06:40:10 +00:00
Json & set ( const char * key , T value ) { String ( key ) ; Int64 ( value ) ; return * this ; }
2015-09-13 20:25:26 +00:00
Json & startObject ( const char * key ) { String ( key ) ; StartObject ( ) ; return * this ; }
Json & startArray ( const char * key ) { String ( key ) ; StartArray ( ) ; return * this ; }
const char * str ( ) { EndObject ( ) ; return buf . GetString ( ) ; }
private :
rapidjson : : StringBuffer buf ;
} ;
2018-09-08 15:16:23 +00:00
template < > Json & Json : : set ( const char * key , double value ) { String ( key ) ; Double ( value ) ; return * this ; }
2015-09-13 20:25:26 +00:00
template < > Json & Json : : set ( const char * key , const char * value ) { String ( key ) ; String ( value ) ; return * this ; }
template < > Json & Json : : set ( const char * key , std : : string value ) { String ( key ) ; String ( value . c_str ( ) ) ; return * this ; }
2018-09-28 07:36:10 +00:00
// short syntax helpers for kj::Path
template < typename T >
inline kj : : Path operator / ( const kj : : Path & p , const T & ext ) {
return p . append ( ext ) ;
}
template < typename T >
inline kj : : Path operator / ( const std : : string & p , const T & ext ) {
return kj : : Path { p } / ext ;
2017-07-31 05:51:46 +00:00
}
2015-09-13 20:25:26 +00:00
typedef std : : string str ;
2019-10-05 17:06:35 +00:00
Laminar : : Laminar ( Server & server , Settings settings ) :
settings ( settings ) ,
srv ( server ) ,
homePath ( kj : : Path : : parse ( & settings . home [ 1 ] ) ) ,
fsHome ( kj : : newDiskFilesystem ( ) - > getRoot ( ) . openSubdir ( homePath , kj : : WriteMode : : MODIFY ) ) ,
http ( kj : : heap < Http > ( * this ) ) ,
rpc ( kj : : heap < Rpc > ( * this ) )
2018-09-28 07:36:10 +00:00
{
2019-10-05 17:06:35 +00:00
LASSERT ( settings . home [ 0 ] = = ' / ' ) ;
2018-09-28 07:36:10 +00:00
2019-12-13 08:42:22 +00:00
if ( fsHome - > exists ( homePath / " cfg " / " nodes " ) ) {
LLOG ( ERROR , " Found node configuration directory cfg/nodes. Nodes have been deprecated, please migrate to contexts. Laminar will now exit. " ) ;
exit ( EXIT_FAILURE ) ;
}
2019-10-05 17:06:35 +00:00
archiveUrl = settings . archive_url ;
if ( archiveUrl . back ( ) ! = ' / ' )
archiveUrl . append ( " / " ) ;
2019-03-27 07:00:13 +00:00
2017-12-09 17:03:43 +00:00
numKeepRunDirs = 0 ;
2015-09-13 20:25:26 +00:00
2018-09-28 07:36:10 +00:00
db = new Database ( ( homePath / " laminar.sqlite " ) . toString ( true ) . cStr ( ) ) ;
2015-09-13 20:25:26 +00:00
// Prepare database for first use
// TODO: error handling
db - > exec ( " CREATE TABLE IF NOT EXISTS builds( "
2015-12-06 10:53:06 +00:00
" name TEXT, number INT UNSIGNED, node TEXT, queuedAt INT, "
" startedAt INT, completedAt INT, result INT, output TEXT, "
2015-12-06 12:47:43 +00:00
" outputLen INT, parentJob TEXT, parentBuild INT, reason TEXT, "
2015-12-06 10:53:06 +00:00
" PRIMARY KEY (name, number)) " ) ;
db - > exec ( " CREATE INDEX IF NOT EXISTS idx_completion_time ON builds( "
" completedAt DESC) " ) ;
2015-09-13 20:25:26 +00:00
// retrieve the last build numbers
db - > stmt ( " SELECT name, MAX(number) FROM builds GROUP BY name " )
2017-12-21 06:19:45 +00:00
. fetch < str , uint > ( [ this ] ( str name , uint build ) {
2015-09-13 20:25:26 +00:00
buildNums [ name ] = build ;
} ) ;
2019-10-05 17:06:35 +00:00
srv . watchPaths ( [ this ] {
LLOG ( INFO , " Reloading configuration " ) ;
loadConfiguration ( ) ;
// config change may allow stuck jobs to dequeue
assignNewJobs ( ) ;
2019-12-13 08:42:22 +00:00
} ) . addPath ( ( homePath / " cfg " / " contexts " ) . toString ( true ) . cStr ( ) )
. addPath ( ( homePath / " cfg " / " jobs " ) . toString ( true ) . cStr ( ) )
. addPath ( ( homePath / " cfg " ) . toString ( true ) . cStr ( ) ) ; // for groups.conf
2019-10-05 17:06:35 +00:00
srv . listenRpc ( * rpc , settings . bind_rpc ) ;
srv . listenHttp ( * http , settings . bind_http ) ;
2018-02-03 14:47:41 +00:00
2018-04-20 09:54:39 +00:00
// Load configuration, may be called again in response to an inotify event
// that the configuration files have been modified
2015-09-13 20:25:26 +00:00
loadConfiguration ( ) ;
}
2019-02-15 17:05:44 +00:00
uint Laminar : : latestRun ( std : : string job ) {
auto it = activeJobs . byJobName ( ) . equal_range ( job ) ;
if ( it . first = = it . second ) {
uint result = 0 ;
2019-02-17 20:51:11 +00:00
db - > stmt ( " SELECT MAX(number) FROM builds WHERE name = ? " )
2019-02-15 17:05:44 +00:00
. bind ( job )
. fetch < uint > ( [ & ] ( uint x ) {
result = x ;
} ) ;
return result ;
} else {
return ( * - - it . second ) - > build ;
}
}
bool Laminar : : handleLogRequest ( std : : string name , uint num , std : : string & output , bool & complete ) {
if ( Run * run = activeRun ( name , num ) ) {
output = run - > log ;
complete = false ;
return true ;
} else { // it must be finished, fetch it from the database
2019-02-17 20:51:11 +00:00
db - > stmt ( " SELECT output, outputLen FROM builds WHERE name = ? AND number = ? " )
2019-02-15 17:05:44 +00:00
. bind ( name , num )
. fetch < str , int > ( [ & ] ( str maybeZipped , unsigned long sz ) {
str log ( sz , ' \0 ' ) ;
if ( sz > = COMPRESS_LOG_MIN_SIZE ) {
int res = : : uncompress ( ( uint8_t * ) log . data ( ) , & sz ,
( const uint8_t * ) maybeZipped . data ( ) , maybeZipped . size ( ) ) ;
if ( res = = Z_OK )
std : : swap ( output , log ) ;
else
LLOG ( ERROR , " Failed to uncompress log " , res ) ;
} else {
std : : swap ( output , maybeZipped ) ;
}
} ) ;
if ( output . size ( ) ) {
complete = true ;
return true ;
}
}
return false ;
}
2017-12-21 06:19:45 +00:00
bool Laminar : : setParam ( std : : string job , uint buildNum , std : : string param , std : : string value ) {
2015-11-01 10:28:22 +00:00
if ( Run * run = activeRun ( job , buildNum ) ) {
run - > params [ param ] = value ;
return true ;
}
return false ;
}
2018-10-12 14:22:21 +00:00
const std : : list < std : : shared_ptr < Run > > & Laminar : : listQueuedJobs ( ) {
return queuedJobs ;
}
const RunSet & Laminar : : listRunningJobs ( ) {
return activeJobs ;
}
std : : list < std : : string > Laminar : : listKnownJobs ( ) {
std : : list < std : : string > res ;
KJ_IF_MAYBE ( dir , fsHome - > tryOpenSubdir ( kj : : Path { " cfg " , " jobs " } ) ) {
for ( kj : : Directory : : Entry & entry : ( * dir ) - > listEntries ( ) ) {
2018-10-14 19:16:42 +00:00
if ( entry . name . endsWith ( " .run " ) ) {
2018-10-12 14:22:21 +00:00
res . emplace_back ( entry . name . cStr ( ) , entry . name . findLast ( ' . ' ) . orDefault ( 0 ) ) ;
}
}
}
return res ;
}
2015-11-01 10:28:22 +00:00
2017-12-21 06:19:45 +00:00
void Laminar : : populateArtifacts ( Json & j , std : : string job , uint num ) const {
2018-09-28 07:36:10 +00:00
kj : : Path runArchive { job , std : : to_string ( num ) } ;
KJ_IF_MAYBE ( dir , fsHome - > tryOpenSubdir ( " archive " / runArchive ) ) {
for ( kj : : StringPtr file : ( * dir ) - > listNames ( ) ) {
kj : : FsNode : : Metadata meta = ( * dir ) - > lstat ( kj : : Path { file } ) ;
if ( meta . type ! = kj : : FsNode : : Type : : FILE )
2015-11-01 10:28:22 +00:00
continue ;
j . StartObject ( ) ;
2018-09-28 07:36:10 +00:00
j . set ( " url " , archiveUrl + ( runArchive / file ) . toString ( ) . cStr ( ) ) ;
j . set ( " filename " , file . cStr ( ) ) ;
j . set ( " size " , meta . size ) ;
2015-11-01 10:28:22 +00:00
j . EndObject ( ) ;
}
}
2015-09-13 20:25:26 +00:00
}
2019-10-05 17:06:35 +00:00
std : : string Laminar : : getStatus ( MonitorScope scope ) {
2015-09-26 20:54:27 +00:00
Json j ;
j . set ( " type " , " status " ) ;
2017-07-13 18:58:19 +00:00
j . set ( " title " , getenv ( " LAMINAR_TITLE " ) ? : " Laminar " ) ;
2018-02-03 14:52:46 +00:00
j . set ( " time " , time ( nullptr ) ) ;
2015-09-26 20:54:27 +00:00
j . startObject ( " data " ) ;
2019-10-05 17:06:35 +00:00
if ( scope . type = = MonitorScope : : RUN ) {
2018-09-30 06:04:17 +00:00
db - > stmt ( " SELECT queuedAt,startedAt,completedAt,result,reason,parentJob,parentBuild FROM builds WHERE name = ? AND number = ? " )
2019-10-05 17:06:35 +00:00
. bind ( scope . job , scope . num )
2018-09-30 06:04:17 +00:00
. fetch < time_t , time_t , time_t , int , std : : string , std : : string , uint > ( [ & ] ( time_t queued , time_t started , time_t completed , int result , std : : string reason , std : : string parentJob , uint parentBuild ) {
2015-09-26 20:54:27 +00:00
j . set ( " queued " , started - queued ) ;
2015-09-13 20:25:26 +00:00
j . set ( " started " , started ) ;
2015-09-26 20:54:27 +00:00
j . set ( " completed " , completed ) ;
2015-09-13 20:25:26 +00:00
j . set ( " result " , to_string ( RunState ( result ) ) ) ;
j . set ( " reason " , reason ) ;
2018-09-30 06:04:17 +00:00
j . startObject ( " upstream " ) . set ( " name " , parentJob ) . set ( " num " , parentBuild ) . EndObject ( 2 ) ;
2015-09-13 20:25:26 +00:00
} ) ;
2019-10-05 17:06:35 +00:00
if ( const Run * run = activeRun ( scope . job , scope . num ) ) {
2015-11-01 10:28:22 +00:00
j . set ( " queued " , run - > startedAt - run - > queuedAt ) ;
j . set ( " started " , run - > startedAt ) ;
2015-12-06 11:15:05 +00:00
j . set ( " result " , to_string ( RunState : : RUNNING ) ) ;
2018-09-30 06:04:17 +00:00
j . set ( " reason " , run - > reason ( ) ) ;
j . startObject ( " upstream " ) . set ( " name " , run - > parentName ) . set ( " num " , run - > parentBuild ) . EndObject ( 2 ) ;
2015-11-01 10:28:22 +00:00
db - > stmt ( " SELECT completedAt - startedAt FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1 " )
. bind ( run - > name )
2017-12-21 06:19:45 +00:00
. fetch < uint > ( [ & ] ( uint lastRuntime ) {
2017-07-13 18:58:39 +00:00
j . set ( " etc " , run - > startedAt + lastRuntime ) ;
2015-11-01 10:28:22 +00:00
} ) ;
}
2019-10-05 17:06:35 +00:00
j . set ( " latestNum " , int ( buildNums [ scope . job ] ) ) ;
2015-09-19 15:24:20 +00:00
j . startArray ( " artifacts " ) ;
2019-10-05 17:06:35 +00:00
populateArtifacts ( j , scope . job , scope . num ) ;
2015-09-19 15:24:20 +00:00
j . EndArray ( ) ;
2019-10-05 17:06:35 +00:00
} else if ( scope . type = = MonitorScope : : JOB ) {
2018-06-01 11:51:34 +00:00
const uint runsPerPage = 10 ;
2015-09-13 20:25:26 +00:00
j . startArray ( " recent " ) ;
2018-08-24 09:15:40 +00:00
// ORDER BY param cannot be bound
std : : string order_by ;
2019-10-05 17:06:35 +00:00
std : : string direction = scope . order_desc ? " DESC " : " ASC " ;
if ( scope . field = = " number " )
2018-08-24 09:15:40 +00:00
order_by = " number " + direction ;
2019-10-05 17:06:35 +00:00
else if ( scope . field = = " result " )
2018-08-24 09:15:40 +00:00
order_by = " result " + direction + " , number DESC " ;
2019-10-05 17:06:35 +00:00
else if ( scope . field = = " started " )
2018-08-24 09:15:40 +00:00
order_by = " startedAt " + direction + " , number DESC " ;
2019-10-05 17:06:35 +00:00
else if ( scope . field = = " duration " )
2018-08-24 09:15:40 +00:00
order_by = " (completedAt-startedAt) " + direction + " , number DESC " ;
else
order_by = " number DESC " ;
std : : string stmt = " SELECT number,startedAt,completedAt,result,reason FROM builds WHERE name = ? ORDER BY "
+ order_by + " LIMIT ?,? " ;
db - > stmt ( stmt . c_str ( ) )
2019-10-05 17:06:35 +00:00
. bind ( scope . job , scope . page * runsPerPage , runsPerPage )
2017-12-21 06:19:45 +00:00
. fetch < uint , time_t , time_t , int , str > ( [ & ] ( uint build , time_t started , time_t completed , int result , str reason ) {
2015-09-13 20:25:26 +00:00
j . StartObject ( ) ;
2015-09-26 20:54:27 +00:00
j . set ( " number " , build )
2017-11-06 17:08:14 +00:00
. set ( " completed " , completed )
2015-09-13 20:25:26 +00:00
. set ( " started " , started )
. set ( " result " , to_string ( RunState ( result ) ) )
2015-09-26 20:54:27 +00:00
. set ( " reason " , reason )
2015-09-13 20:25:26 +00:00
. EndObject ( ) ;
} ) ;
j . EndArray ( ) ;
2018-09-08 18:02:58 +00:00
db - > stmt ( " SELECT COUNT(*),AVG(completedAt-startedAt) FROM builds WHERE name = ? " )
2019-10-05 17:06:35 +00:00
. bind ( scope . job )
2018-09-08 18:02:58 +00:00
. fetch < uint , uint > ( [ & ] ( uint nRuns , uint averageRuntime ) {
j . set ( " averageRuntime " , averageRuntime ) ;
2018-08-24 09:15:40 +00:00
j . set ( " pages " , ( nRuns - 1 ) / runsPerPage + 1 ) ;
j . startObject ( " sort " ) ;
2019-10-05 17:06:35 +00:00
j . set ( " page " , scope . page )
. set ( " field " , scope . field )
. set ( " order " , scope . order_desc ? " dsc " : " asc " )
2018-08-24 09:15:40 +00:00
. EndObject ( ) ;
2018-06-01 11:51:34 +00:00
} ) ;
2015-09-13 20:25:26 +00:00
j . startArray ( " running " ) ;
2019-10-05 17:06:35 +00:00
auto p = activeJobs . byJobName ( ) . equal_range ( scope . job ) ;
2015-09-13 20:25:26 +00:00
for ( auto it = p . first ; it ! = p . second ; + + it ) {
const std : : shared_ptr < Run > run = * it ;
j . StartObject ( ) ;
j . set ( " number " , run - > build ) ;
2019-12-13 08:42:22 +00:00
j . set ( " context " , run - > context - > name ) ;
2015-09-13 20:25:26 +00:00
j . set ( " started " , run - > startedAt ) ;
2015-12-06 11:15:05 +00:00
j . set ( " result " , to_string ( RunState : : RUNNING ) ) ;
2015-11-01 10:34:18 +00:00
j . set ( " reason " , run - > reason ( ) ) ;
2015-09-13 20:25:26 +00:00
j . EndObject ( ) ;
}
j . EndArray ( ) ;
2015-09-26 20:54:27 +00:00
int nQueued = 0 ;
2017-12-21 06:19:45 +00:00
for ( const auto & run : queuedJobs ) {
2019-10-05 17:06:35 +00:00
if ( run - > name = = scope . job ) {
2015-09-26 20:54:27 +00:00
nQueued + + ;
2015-09-13 20:25:26 +00:00
}
}
2015-09-26 20:54:27 +00:00
j . set ( " nQueued " , nQueued ) ;
db - > stmt ( " SELECT number,startedAt FROM builds WHERE name = ? AND result = ? ORDER BY completedAt DESC LIMIT 1 " )
2019-10-05 17:06:35 +00:00
. bind ( scope . job , int ( RunState : : SUCCESS ) )
2015-09-26 20:54:27 +00:00
. fetch < int , time_t > ( [ & ] ( int build , time_t started ) {
j . startObject ( " lastSuccess " ) ;
j . set ( " number " , build ) . set ( " started " , started ) ;
j . EndObject ( ) ;
} ) ;
db - > stmt ( " SELECT number,startedAt FROM builds WHERE name = ? AND result <> ? ORDER BY completedAt DESC LIMIT 1 " )
2019-10-05 17:06:35 +00:00
. bind ( scope . job , int ( RunState : : SUCCESS ) )
2015-09-26 20:54:27 +00:00
. fetch < int , time_t > ( [ & ] ( int build , time_t started ) {
j . startObject ( " lastFailed " ) ;
j . set ( " number " , build ) . set ( " started " , started ) ;
j . EndObject ( ) ;
} ) ;
2019-12-24 20:10:16 +00:00
auto desc = jobDescriptions . find ( scope . job ) ;
j . set ( " description " , desc = = jobDescriptions . end ( ) ? " " : desc - > second ) ;
2019-10-05 17:06:35 +00:00
} else if ( scope . type = = MonitorScope : : ALL ) {
2015-09-13 20:25:26 +00:00
j . startArray ( " jobs " ) ;
2018-09-30 10:08:34 +00:00
db - > stmt ( " SELECT name,number,startedAt,completedAt,result FROM builds b JOIN (SELECT name n,MAX(number) l FROM builds GROUP BY n) q ON b.name = q.n AND b.number = q.l " )
2017-12-21 06:19:45 +00:00
. fetch < str , uint , time_t , time_t , int > ( [ & ] ( str name , uint number , time_t started , time_t completed , int result ) {
2015-09-13 20:25:26 +00:00
j . StartObject ( ) ;
2015-09-26 20:54:27 +00:00
j . set ( " name " , name ) ;
2017-11-07 06:21:01 +00:00
j . set ( " number " , number ) ;
j . set ( " result " , to_string ( RunState ( result ) ) ) ;
j . set ( " started " , started ) ;
j . set ( " completed " , completed ) ;
2015-09-26 20:54:27 +00:00
j . EndObject ( ) ;
2015-09-13 20:25:26 +00:00
} ) ;
j . EndArray ( ) ;
2017-11-07 06:21:01 +00:00
j . startArray ( " running " ) ;
2017-12-21 06:19:45 +00:00
for ( const auto & run : activeJobs . byStartedAt ( ) ) {
2017-11-07 06:21:01 +00:00
j . StartObject ( ) ;
j . set ( " name " , run - > name ) ;
j . set ( " number " , run - > build ) ;
2019-12-13 08:42:22 +00:00
j . set ( " context " , run - > context - > name ) ;
2017-11-07 06:21:01 +00:00
j . set ( " started " , run - > startedAt ) ;
j . EndObject ( ) ;
}
j . EndArray ( ) ;
2019-12-13 08:42:22 +00:00
j . startObject ( " groups " ) ;
for ( const auto & group : jobGroups )
j . set ( group . first . c_str ( ) , group . second ) ;
j . EndObject ( ) ;
2015-09-13 20:25:26 +00:00
} else { // Home page
j . startArray ( " recent " ) ;
2015-09-26 20:54:27 +00:00
db - > stmt ( " SELECT * FROM builds ORDER BY completedAt DESC LIMIT 15 " )
2019-12-13 08:42:22 +00:00
. fetch < str , uint , str , time_t , time_t , time_t , int > ( [ & ] ( str name , uint build , str context , time_t , time_t started , time_t completed , int result ) {
2015-09-13 20:25:26 +00:00
j . StartObject ( ) ;
j . set ( " name " , name )
. set ( " number " , build )
2019-12-13 08:42:22 +00:00
. set ( " context " , context )
2015-09-13 20:25:26 +00:00
. set ( " started " , started )
2017-11-06 17:08:14 +00:00
. set ( " completed " , completed )
2015-09-13 20:25:26 +00:00
. set ( " result " , to_string ( RunState ( result ) ) )
. EndObject ( ) ;
} ) ;
j . EndArray ( ) ;
j . startArray ( " running " ) ;
2017-12-21 06:19:45 +00:00
for ( const auto & run : activeJobs . byStartedAt ( ) ) {
2015-09-13 20:25:26 +00:00
j . StartObject ( ) ;
j . set ( " name " , run - > name ) ;
j . set ( " number " , run - > build ) ;
2019-12-13 08:42:22 +00:00
j . set ( " context " , run - > context - > name ) ;
2015-09-13 20:25:26 +00:00
j . set ( " started " , run - > startedAt ) ;
2015-11-01 10:34:18 +00:00
db - > stmt ( " SELECT completedAt - startedAt FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1 " )
. bind ( run - > name )
2017-12-21 06:19:45 +00:00
. fetch < uint > ( [ & ] ( uint lastRuntime ) {
2017-07-13 18:58:39 +00:00
j . set ( " etc " , run - > startedAt + lastRuntime ) ;
2015-11-01 10:34:18 +00:00
} ) ;
2015-09-13 20:25:26 +00:00
j . EndObject ( ) ;
}
j . EndArray ( ) ;
j . startArray ( " queued " ) ;
2017-12-21 06:19:45 +00:00
for ( const auto & run : queuedJobs ) {
2015-09-13 20:25:26 +00:00
j . StartObject ( ) ;
j . set ( " name " , run - > name ) ;
j . EndObject ( ) ;
}
j . EndArray ( ) ;
int execTotal = 0 ;
int execBusy = 0 ;
2019-12-13 08:42:22 +00:00
for ( const auto & it : contexts ) {
const std : : shared_ptr < Context > & context = it . second ;
execTotal + = context - > numExecutors ;
execBusy + = context - > busyExecutors ;
2015-09-13 20:25:26 +00:00
}
j . set ( " executorsTotal " , execTotal ) ;
j . set ( " executorsBusy " , execBusy ) ;
j . startArray ( " buildsPerDay " ) ;
for ( int i = 6 ; i > = 0 ; - - i ) {
j . StartObject ( ) ;
2018-09-08 15:16:23 +00:00
db - > stmt ( " SELECT result, COUNT(*) FROM builds WHERE completedAt > ? AND completedAt < ? GROUP BY result " )
2017-12-21 06:19:45 +00:00
. bind ( 86400 * ( time ( nullptr ) / 86400 - i ) , 86400 * ( time ( nullptr ) / 86400 - ( i - 1 ) ) )
2015-09-13 20:25:26 +00:00
. fetch < int , int > ( [ & ] ( int result , int num ) {
j . set ( to_string ( RunState ( result ) ) . c_str ( ) , num ) ;
} ) ;
j . EndObject ( ) ;
}
j . EndArray ( ) ;
j . startObject ( " buildsPerJob " ) ;
2018-06-01 07:33:25 +00:00
db - > stmt ( " SELECT name, COUNT(*) c FROM builds WHERE completedAt > ? GROUP BY name ORDER BY c DESC LIMIT 5 " )
2017-12-21 06:19:45 +00:00
. bind ( time ( nullptr ) - 86400 )
2015-09-13 20:25:26 +00:00
. fetch < str , int > ( [ & ] ( str job , int count ) {
j . set ( job . c_str ( ) , count ) ;
} ) ;
j . EndObject ( ) ;
2015-09-26 20:54:27 +00:00
j . startObject ( " timePerJob " ) ;
2018-09-08 15:16:23 +00:00
db - > stmt ( " SELECT name, AVG(completedAt-startedAt) av FROM builds WHERE completedAt > ? GROUP BY name ORDER BY av DESC LIMIT 8 " )
2017-12-21 06:19:45 +00:00
. bind ( time ( nullptr ) - 7 * 86400 )
. fetch < str , uint > ( [ & ] ( str job , uint time ) {
2015-09-26 20:54:27 +00:00
j . set ( job . c_str ( ) , time ) ;
} ) ;
2015-09-13 20:25:26 +00:00
j . EndObject ( ) ;
2018-09-08 15:16:23 +00:00
j . startArray ( " resultChanged " ) ;
db - > stmt ( " SELECT b.name,MAX(b.number) as lastSuccess,lastFailure FROM builds AS b JOIN (SELECT name,MAX(number) AS lastFailure FROM builds WHERE result<>? GROUP BY name) AS t ON t.name=b.name WHERE b.result=? GROUP BY b.name ORDER BY lastSuccess>lastFailure, lastFailure-lastSuccess DESC LIMIT 8 " )
. bind ( int ( RunState : : SUCCESS ) , int ( RunState : : SUCCESS ) )
. fetch < str , uint , uint > ( [ & ] ( str job , uint lastSuccess , uint lastFailure ) {
j . StartObject ( ) ;
j . set ( " name " , job )
. set ( " lastSuccess " , lastSuccess )
. set ( " lastFailure " , lastFailure ) ;
j . EndObject ( ) ;
} ) ;
j . EndArray ( ) ;
j . startArray ( " lowPassRates " ) ;
db - > stmt ( " SELECT name,CAST(SUM(result==?) AS FLOAT)/COUNT(*) AS passRate FROM builds GROUP BY name ORDER BY passRate ASC LIMIT 8 " )
. bind ( int ( RunState : : SUCCESS ) )
. fetch < str , double > ( [ & ] ( str job , double passRate ) {
j . StartObject ( ) ;
j . set ( " name " , job ) . set ( " passRate " , passRate ) ;
j . EndObject ( ) ;
} ) ;
j . EndArray ( ) ;
j . startArray ( " buildTimeChanges " ) ;
db - > stmt ( " SELECT name,GROUP_CONCAT(number),GROUP_CONCAT(completedAt-startedAt) FROM builds WHERE number > (SELECT MAX(number)-10 FROM builds b WHERE b.name=builds.name) GROUP BY name ORDER BY (MAX(completedAt-startedAt)-MIN(completedAt-startedAt))-STDEV(completedAt-startedAt) DESC LIMIT 8 " )
. fetch < str , str , str > ( [ & ] ( str name , str numbers , str durations ) {
j . StartObject ( ) ;
j . set ( " name " , name ) ;
j . startArray ( " numbers " ) ;
j . RawValue ( numbers . data ( ) , numbers . length ( ) , rapidjson : : Type : : kArrayType ) ;
j . EndArray ( ) ;
j . startArray ( " durations " ) ;
j . RawValue ( durations . data ( ) , durations . length ( ) , rapidjson : : Type : : kArrayType ) ;
j . EndArray ( ) ;
j . EndObject ( ) ;
} ) ;
j . EndArray ( ) ;
j . startArray ( " buildTimeDist " ) ;
db - > stmt ( " WITH ba AS (SELECT name,AVG(completedAt-startedAt) a FROM builds GROUP BY name) SELECT "
" COUNT(CASE WHEN a < 30 THEN 1 END), "
" COUNT(CASE WHEN a >= 30 AND a < 60 THEN 1 END), "
" COUNT(CASE WHEN a >= 60 AND a < 300 THEN 1 END), "
" COUNT(CASE WHEN a >= 300 AND a < 600 THEN 1 END), "
" COUNT(CASE WHEN a >= 600 AND a < 1200 THEN 1 END), "
" COUNT(CASE WHEN a >= 1200 AND a < 2400 THEN 1 END), "
" COUNT(CASE WHEN a >= 2400 AND a < 3600 THEN 1 END), "
" COUNT(CASE WHEN a >= 3600 THEN 1 END) FROM ba " )
. fetch < uint , uint , uint , uint , uint , uint , uint , uint > ( [ & ] ( uint c1 , uint c2 , uint c3 , uint c4 , uint c5 , uint c6 , uint c7 , uint c8 ) {
j . Int ( c1 ) ;
j . Int ( c2 ) ;
j . Int ( c3 ) ;
j . Int ( c4 ) ;
j . Int ( c5 ) ;
j . Int ( c6 ) ;
j . Int ( c7 ) ;
j . Int ( c8 ) ;
} ) ;
j . EndArray ( ) ;
2015-09-26 20:54:27 +00:00
2015-09-13 20:25:26 +00:00
}
2015-09-26 20:54:27 +00:00
j . EndObject ( ) ;
2019-10-05 17:06:35 +00:00
return j . str ( ) ;
2015-09-13 20:25:26 +00:00
}
2018-09-28 07:36:10 +00:00
Laminar : : ~ Laminar ( ) noexcept try {
2015-09-13 20:25:26 +00:00
delete db ;
2018-09-28 07:36:10 +00:00
} catch ( std : : exception & e ) {
LLOG ( ERROR , e . what ( ) ) ;
return ;
2015-09-13 20:25:26 +00:00
}
bool Laminar : : loadConfiguration ( ) {
2017-12-09 17:03:43 +00:00
if ( const char * ndirs = getenv ( " LAMINAR_KEEP_RUNDIRS " ) )
2017-12-21 06:19:45 +00:00
numKeepRunDirs = static_cast < uint > ( atoi ( ndirs ) ) ;
2015-09-19 13:29:07 +00:00
2019-12-13 08:42:22 +00:00
std : : set < std : : string > knownContexts ;
2015-09-13 20:25:26 +00:00
2019-12-13 08:42:22 +00:00
KJ_IF_MAYBE ( contextsDir , fsHome - > tryOpenSubdir ( kj : : Path { " cfg " , " contexts " } ) ) {
for ( kj : : Directory : : Entry & entry : ( * contextsDir ) - > listEntries ( ) ) {
2018-10-14 19:16:42 +00:00
if ( ! entry . name . endsWith ( " .conf " ) )
2015-09-19 12:36:03 +00:00
continue ;
2015-09-13 20:25:26 +00:00
2019-12-13 08:42:22 +00:00
StringMap conf = parseConfFile ( ( homePath / " cfg " / " contexts " / entry . name ) . toString ( true ) . cStr ( ) ) ;
std : : string name ( entry . name . cStr ( ) , entry . name . findLast ( ' . ' ) . orDefault ( 0 ) ) ;
auto existing = contexts . find ( name ) ;
std : : shared_ptr < Context > context = existing = = contexts . end ( ) ? contexts . emplace ( name , std : : shared_ptr < Context > ( new Context ) ) . first - > second : existing - > second ;
context - > name = name ;
context - > numExecutors = conf . get < int > ( " EXECUTORS " , 6 ) ;
2015-09-24 20:02:11 +00:00
2019-12-13 08:42:22 +00:00
knownContexts . insert ( name ) ;
2015-09-19 12:36:03 +00:00
}
2015-09-13 20:25:26 +00:00
}
2019-12-13 08:42:22 +00:00
// remove any contexts whose config files disappeared.
// if there are no known contexts, take care not to remove and re-add the default context.
for ( auto it = contexts . begin ( ) ; it ! = contexts . end ( ) ; ) {
if ( ( it - > first = = " default " & & knownContexts . size ( ) = = 0 ) | | knownContexts . find ( it - > first ) ! = knownContexts . end ( ) )
2018-04-20 11:18:10 +00:00
it + + ;
else
2019-12-13 08:42:22 +00:00
it = contexts . erase ( it ) ;
2015-09-13 20:25:26 +00:00
}
2019-12-13 08:42:22 +00:00
// add a default context
if ( contexts . empty ( ) ) {
LLOG ( INFO , " Creating a default context with 6 executors " ) ;
std : : shared_ptr < Context > context ( new Context ) ;
context - > name = " default " ;
context - > numExecutors = 6 ;
contexts . emplace ( " default " , context ) ;
2018-04-20 11:18:10 +00:00
}
2015-09-13 20:25:26 +00:00
2018-09-28 07:36:10 +00:00
KJ_IF_MAYBE ( jobsDir , fsHome - > tryOpenSubdir ( kj : : Path { " cfg " , " jobs " } ) ) {
for ( kj : : Directory : : Entry & entry : ( * jobsDir ) - > listEntries ( ) ) {
2018-10-14 19:16:42 +00:00
if ( ! entry . name . endsWith ( " .conf " ) )
2015-09-24 20:02:11 +00:00
continue ;
2018-10-12 14:01:42 +00:00
StringMap conf = parseConfFile ( ( homePath / " cfg " / " jobs " / entry . name ) . toString ( true ) . cStr ( ) ) ;
2015-09-24 20:02:11 +00:00
2018-10-12 14:01:42 +00:00
std : : string jobName ( entry . name . cStr ( ) , entry . name . findLast ( ' . ' ) . orDefault ( 0 ) ) ;
2015-09-24 20:02:11 +00:00
2019-12-13 08:42:22 +00:00
std : : string ctxPtns = conf . get < std : : string > ( " CONTEXTS " ) ;
2015-09-24 20:02:11 +00:00
2019-12-13 08:42:22 +00:00
if ( ! ctxPtns . empty ( ) ) {
std : : istringstream iss ( ctxPtns ) ;
std : : set < std : : string > ctxPtnList ;
std : : string ctx ;
while ( std : : getline ( iss , ctx , ' , ' ) )
ctxPtnList . insert ( ctx ) ;
jobContexts [ jobName ] . swap ( ctxPtnList ) ;
}
2019-12-24 20:10:16 +00:00
std : : string desc = conf . get < std : : string > ( " DESCRIPTION " ) ;
if ( ! desc . empty ( ) ) {
jobDescriptions [ jobName ] = desc ;
}
2015-09-24 20:02:11 +00:00
}
}
2019-12-13 08:42:22 +00:00
jobGroups . clear ( ) ;
KJ_IF_MAYBE ( groupsConf , fsHome - > tryOpenFile ( kj : : Path { " cfg " , " groups.conf " } ) )
jobGroups = parseConfFile ( ( homePath / " cfg " / " groups.conf " ) . toString ( true ) . cStr ( ) ) ;
if ( jobGroups . empty ( ) )
jobGroups [ " All Jobs " ] = " .* " ;
2015-09-13 20:25:26 +00:00
return true ;
}
std : : shared_ptr < Run > Laminar : : queueJob ( std : : string name , ParamMap params ) {
2018-09-28 07:36:10 +00:00
if ( ! fsHome - > exists ( kj : : Path { " cfg " , " jobs " , name + " .run " } ) ) {
2015-12-06 11:36:12 +00:00
LLOG ( ERROR , " Non-existent job " , name ) ;
2015-09-13 20:25:26 +00:00
return nullptr ;
2015-11-01 10:35:07 +00:00
}
2015-09-13 20:25:26 +00:00
2019-12-13 08:42:22 +00:00
// If the job has no contexts (maybe there is no .conf file at all), add the default context
if ( jobContexts [ name ] . empty ( ) )
jobContexts . at ( name ) . insert ( " default " ) ;
2018-09-28 07:36:10 +00:00
std : : shared_ptr < Run > run = std : : make_shared < Run > ( name , kj : : mv ( params ) , homePath . clone ( ) ) ;
2015-09-13 20:25:26 +00:00
queuedJobs . push_back ( run ) ;
// notify clients
Json j ;
j . set ( " type " , " job_queued " )
. startObject ( " data " )
. set ( " name " , name )
. EndObject ( ) ;
2019-11-01 05:27:34 +00:00
http - > notifyEvent ( j . str ( ) , name . c_str ( ) ) ;
2015-09-13 20:25:26 +00:00
assignNewJobs ( ) ;
return run ;
}
2018-10-12 14:22:21 +00:00
bool Laminar : : abort ( std : : string job , uint buildNum ) {
2019-12-21 13:29:37 +00:00
if ( Run * run = activeRun ( job , buildNum ) )
return run - > abort ( ) ;
2018-10-12 14:22:21 +00:00
return false ;
}
2018-02-24 16:53:11 +00:00
void Laminar : : abortAll ( ) {
for ( std : : shared_ptr < Run > run : activeJobs ) {
2019-12-21 13:29:37 +00:00
run - > abort ( ) ;
2018-02-24 16:53:11 +00:00
}
2015-09-13 20:25:26 +00:00
}
2018-07-20 11:15:59 +00:00
bool Laminar : : tryStartRun ( std : : shared_ptr < Run > run , int queueIndex ) {
2019-12-13 08:42:22 +00:00
for ( auto & sc : contexts ) {
std : : shared_ptr < Context > ctx = sc . second ;
2018-05-12 10:25:19 +00:00
2019-12-21 13:29:37 +00:00
if ( ctx - > canQueue ( jobContexts . at ( run - > name ) ) ) {
kj : : Promise < RunState > onRunFinished = run - > start ( buildNums [ run - > name ] + 1 , ctx , * fsHome , [ this ] ( kj : : Maybe < pid_t > & pid ) { return srv . onChildExit ( pid ) ; } ) ;
2019-12-13 08:42:22 +00:00
ctx - > busyExecutors + + ;
2018-07-20 11:15:59 +00:00
// set the last known result if exists
db - > stmt ( " SELECT result FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1 " )
. bind ( run - > name )
. fetch < int > ( [ = ] ( int result ) {
run - > lastResult = RunState ( result ) ;
} ) ;
2019-12-21 13:29:37 +00:00
kj : : Promise < void > exec = srv . readDescriptor ( run - > output_fd , [ this , run ] ( const char * b , size_t n ) {
// handle log output
std : : string s ( b , n ) ;
run - > log + = s ;
http - > notifyLog ( run - > name , run - > build , s , false ) ;
} ) . then ( [ run , p = kj : : mv ( onRunFinished ) ] ( ) mutable {
// wait until leader reaped
return kj : : mv ( p ) ;
} ) . then ( [ this , run ] ( RunState ) {
handleRunFinished ( run . get ( ) ) ;
2018-09-28 07:36:10 +00:00
} ) ;
if ( run - > timeout > 0 ) {
2019-10-05 17:06:35 +00:00
exec = exec . attach ( srv . addTimeout ( run - > timeout , [ r = run . get ( ) ] ( ) {
2019-12-21 13:29:37 +00:00
r - > abort ( ) ;
2018-09-28 07:36:10 +00:00
} ) ) ;
}
2019-10-05 17:06:35 +00:00
srv . addTask ( kj : : mv ( exec ) ) ;
2019-12-13 08:42:22 +00:00
LLOG ( INFO , " Started job " , run - > name , run - > build , ctx - > name ) ;
2018-09-28 07:36:10 +00:00
// update next build number
buildNums [ run - > name ] + + ;
2018-07-20 11:15:59 +00:00
// notify clients
Json j ;
j . set ( " type " , " job_started " )
. startObject ( " data " )
. set ( " queueIndex " , queueIndex )
. set ( " name " , run - > name )
. set ( " queued " , run - > startedAt - run - > queuedAt )
. set ( " started " , run - > startedAt )
. set ( " number " , run - > build )
. set ( " reason " , run - > reason ( ) ) ;
db - > stmt ( " SELECT completedAt - startedAt FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1 " )
. bind ( run - > name )
. fetch < uint > ( [ & ] ( uint etc ) {
j . set ( " etc " , time ( nullptr ) + etc ) ;
} ) ;
j . EndObject ( ) ;
2019-11-01 05:27:34 +00:00
http - > notifyEvent ( j . str ( ) , run - > name . c_str ( ) ) ;
2018-07-20 11:15:59 +00:00
return true ;
2015-09-13 20:25:26 +00:00
}
2018-07-20 11:15:59 +00:00
}
return false ;
}
void Laminar : : assignNewJobs ( ) {
auto it = queuedJobs . begin ( ) ;
while ( it ! = queuedJobs . end ( ) ) {
if ( tryStartRun ( * it , std : : distance ( it , queuedJobs . begin ( ) ) ) ) {
2015-09-13 20:25:26 +00:00
activeJobs . insert ( * it ) ;
it = queuedJobs . erase ( it ) ;
2018-07-20 11:15:59 +00:00
} else {
2015-09-13 20:25:26 +00:00
+ + it ;
2018-07-20 11:15:59 +00:00
}
}
}
2015-09-13 20:25:26 +00:00
2019-12-21 13:29:37 +00:00
void Laminar : : handleRunFinished ( Run * r ) {
2019-12-13 08:42:22 +00:00
std : : shared_ptr < Context > ctx = r - > context ;
2015-09-19 13:41:19 +00:00
2019-12-13 08:42:22 +00:00
ctx - > busyExecutors - - ;
2015-12-06 11:36:12 +00:00
LLOG ( INFO , " Run completed " , r - > name , to_string ( r - > result ) ) ;
2017-12-21 06:19:45 +00:00
time_t completedAt = time ( nullptr ) ;
2015-12-06 12:47:43 +00:00
// compress log
2017-07-31 05:56:58 +00:00
std : : string maybeZipped = r - > log ;
2015-12-06 12:47:43 +00:00
size_t logsize = r - > log . length ( ) ;
2017-07-31 05:56:58 +00:00
if ( r - > log . length ( ) > = COMPRESS_LOG_MIN_SIZE ) {
std : : string zipped ( r - > log . size ( ) , ' \0 ' ) ;
2017-09-22 16:00:55 +00:00
unsigned long zippedSize = zipped . size ( ) ;
2017-12-21 06:19:45 +00:00
if ( : : compress ( ( uint8_t * ) zipped . data ( ) , & zippedSize ,
( const uint8_t * ) r - > log . data ( ) , logsize ) = = Z_OK ) {
2017-07-31 05:56:58 +00:00
zipped . resize ( zippedSize ) ;
std : : swap ( maybeZipped , zipped ) ;
}
}
2015-12-06 12:47:43 +00:00
2016-07-23 15:07:33 +00:00
std : : string reason = r - > reason ( ) ;
2015-12-06 12:47:43 +00:00
db - > stmt ( " INSERT INTO builds VALUES(?,?,?,?,?,?,?,?,?,?,?,?) " )
2019-12-13 08:42:22 +00:00
. bind ( r - > name , r - > build , ctx - > name , r - > queuedAt , r - > startedAt , completedAt , int ( r - > result ) ,
2017-07-31 05:56:58 +00:00
maybeZipped , logsize , r - > parentName , r - > parentBuild , reason )
2015-09-19 13:41:19 +00:00
. exec ( ) ;
// notify clients
Json j ;
j . set ( " type " , " job_completed " )
. startObject ( " data " )
. set ( " name " , r - > name )
. set ( " number " , r - > build )
2015-11-01 10:34:18 +00:00
. set ( " queued " , r - > startedAt - r - > queuedAt )
. set ( " completed " , completedAt )
2015-09-19 13:41:19 +00:00
. set ( " started " , r - > startedAt )
. set ( " result " , to_string ( r - > result ) )
2015-11-01 10:28:22 +00:00
. set ( " reason " , r - > reason ( ) ) ;
j . startArray ( " artifacts " ) ;
populateArtifacts ( j , r - > name , r - > build ) ;
j . EndArray ( ) ;
j . EndObject ( ) ;
2019-11-01 05:27:34 +00:00
http - > notifyEvent ( j . str ( ) , r - > name ) ;
2019-10-05 17:06:35 +00:00
http - > notifyLog ( r - > name , r - > build , " " , true ) ;
2018-08-03 11:36:24 +00:00
// erase reference to run from activeJobs. Since runFinished is called in a
// lambda whose context contains a shared_ptr<Run>, the run won't be deleted
// until the context is destroyed at the end of the lambda execution.
2017-12-20 07:02:12 +00:00
activeJobs . byRunPtr ( ) . erase ( r ) ;
2017-12-09 17:03:43 +00:00
// remove old run directories
// We cannot count back the number of directories to keep from the currently
// finishing job because there may well be older, still-running instances of
// this job and we don't want to delete their rundirs. So instead, check
// whether there are any more active runs of this job, and if so, count back
// from the oldest among them. If there are none, count back from the latest
// known build number of this job, which may not be that of the run that
// finished here.
2017-12-20 07:02:12 +00:00
auto it = activeJobs . byJobName ( ) . equal_range ( r - > name ) ;
2017-12-09 17:03:43 +00:00
uint oldestActive = ( it . first = = it . second ) ? buildNums [ r - > name ] : ( * it . first ) - > build - 1 ;
2017-12-21 06:19:45 +00:00
for ( int i = static_cast < int > ( oldestActive - numKeepRunDirs ) ; i > 0 ; i - - ) {
2018-09-28 07:36:10 +00:00
kj : : Path d { " run " , r - > name , std : : to_string ( i ) } ;
2017-12-09 17:03:43 +00:00
// Once the directory does not exist, it's probably not worth checking
// any further. 99% of the time this loop should only ever have 1 iteration
// anyway so hence this (admittedly debatable) optimization.
2018-09-28 07:36:10 +00:00
if ( ! fsHome - > exists ( d ) )
2017-12-09 17:03:43 +00:00
break ;
2018-09-28 07:36:10 +00:00
fsHome - > remove ( d ) ;
2017-12-09 17:03:43 +00:00
}
2018-08-03 11:36:24 +00:00
2019-02-18 21:06:11 +00:00
fsHome - > symlink ( kj : : Path { " archive " , r - > name , " latest " } , std : : to_string ( r - > build ) , kj : : WriteMode : : CREATE | kj : : WriteMode : : MODIFY ) ;
2018-08-03 11:36:24 +00:00
// in case we freed up an executor, check the queue
assignNewJobs ( ) ;
2015-09-19 13:41:19 +00:00
}
2015-09-19 15:24:20 +00:00
2018-09-28 07:36:10 +00:00
kj : : Maybe < kj : : Own < const kj : : ReadableFile > > Laminar : : getArtefact ( std : : string path ) {
return fsHome - > openFile ( kj : : Path ( " archive " ) . append ( kj : : Path : : parse ( path ) ) ) ;
2017-12-29 09:14:10 +00:00
}
2018-09-10 11:51:43 +00:00
bool Laminar : : handleBadgeRequest ( std : : string job , std : : string & badge ) {
RunState rs = RunState : : UNKNOWN ;
db - > stmt ( " SELECT result FROM builds WHERE name = ? ORDER BY number DESC LIMIT 1 " )
. bind ( job )
. fetch < int > ( [ & ] ( int result ) {
2019-11-01 05:27:34 +00:00
rs = RunState ( result ) ;
2018-09-10 11:51:43 +00:00
} ) ;
if ( rs = = RunState : : UNKNOWN )
return false ;
std : : string status = to_string ( rs ) ;
// Empirical approximation of pixel width. Not particularly stable.
const int jobNameWidth = job . size ( ) * 7 + 10 ;
const int statusWidth = status . size ( ) * 7 + 10 ;
const char * gradient1 = ( rs = = RunState : : SUCCESS ) ? " #2aff4d " : " #ff2a2a " ;
const char * gradient2 = ( rs = = RunState : : SUCCESS ) ? " #24b43c " : " #b42424 " ;
char * svg = NULL ;
asprintf ( & svg ,
R " x(
2018-10-12 09:56:16 +00:00
< svg xmlns = " http://www.w3.org/2000/svg " width = " %d " height = " 20 " >
2018-09-10 11:51:43 +00:00
< clipPath id = " clip " >
< rect width = " %d " height = " 20 " rx = " 4 " / >
< / clipPath >
< linearGradient id = " job " x1 = " 0 " x2 = " 0 " y1 = " 0 " y2 = " 1 " >
< stop offset = " 0 " stop - color = " #666 " / >
< stop offset = " 1 " stop - color = " #333 " / >
< / linearGradient >
< linearGradient id = " status " x1 = " 0 " x2 = " 0 " y1 = " 0 " y2 = " 1 " >
< stop offset = " 0 " stop - color = " %s " / >
< stop offset = " 1 " stop - color = " %s " / >
< / linearGradient >
< g clip - path = " url(#clip) " font - family = " DejaVu Sans,Verdana,sans-serif " font - size = " 12 " text - anchor = " middle " >
< rect width = " %d " height = " 20 " fill = " url(#job) " / >
< text x = " %d " y = " 14 " fill = " #fff " > % s < / text >
< rect x = " %d " width = " %d " height = " 20 " fill = " url(#status) " / >
< text x = " %d " y = " 14 " fill = " #000 " > % s < / text >
< / g >
2018-10-12 09:56:16 +00:00
< / svg > ) x " , jobNameWidth+statusWidth, jobNameWidth+statusWidth, gradient1, gradient2, jobNameWidth, jobNameWidth/2+1, job.data(), jobNameWidth, statusWidth, jobNameWidth+statusWidth/2, status.data());
2018-09-10 11:51:43 +00:00
badge = svg ;
return true ;
}
2018-07-06 10:45:13 +00:00
std : : string Laminar : : getCustomCss ( ) {
2018-09-28 07:36:10 +00:00
KJ_IF_MAYBE ( cssFile , fsHome - > tryOpenFile ( kj : : Path { " custom " , " style.css " } ) ) {
return ( * cssFile ) - > readAllText ( ) . cStr ( ) ;
} else {
return std : : string ( ) ;
2018-07-06 10:45:13 +00:00
}
2017-12-29 09:14:10 +00:00
}