def next(build_hosts: List[Build_Cluster.Host]): Option[Task] = { val cluster_running = running.values.exists(_.build_cluster) val available = build_hosts.map(_.hostname).toSet -- running.values.flatMap(_.hostnames).toSet val ready = for {
(_, task) <- pending if !task.build_cluster || !cluster_running if task.build_hosts.map(_.hostname).forall(available.contains)
} yield task
if (ready.isEmpty) None else { val priority = ready.map(_.priority).maxBy(_.ordinal)
ready.filter(_.priority == priority).toList.sortBy(_.submit_date)(Date.Ordering).headOption
}
}
def add_running(job: Job): State = copy(running = running + (job.name -> job)) def remove_running(name: String): State = copy(running = running - name) def cancel_running(name: String): State =
running.get(name) match { case Some(job) => copy(running = (running - name) + (name -> job.copy(cancelled = true))) case None => this
}
object State { val serial = SQL.Column.long("serial").make_primary_key
val table = make_table(List(serial), name = "state")
}
def read_serial(db: SQL.Database): Long =
db.execute_query_statementO[Long](
State.table.select(List(State.serial.max)),
_.long(State.serial)).getOrElse(0L)
def pull_state(db: SQL.Database, state: State): State = { val serial_db = read_serial(db) if (serial_db == state.serial) state else { val serial = serial_db max state.serial
val pending = pull_pending(db) val running = pull_running(db) val finished = pull_finished(db, state.finished)
def push_state(db: SQL.Database, old_state: State, state: State): State = { val finished = push_finished(db, state.finished) val updates =
List(
update_pending(db, old_state.pending, state.pending),
update_running(db, old_state.running, state.running),
).filter(_.defined)
if (updates.isEmpty && finished == old_state.finished) state else { val serial = state.next_serial
db.execute_statement(State.table.delete(State.serial.where_equal(old_state.serial)))
db.execute_statement(State.table.insert(), body =
{ (stmt: SQL.Statement) =>
stmt.long(1) = serial
})
state.copy(serial = serial, finished = finished)
}
}
/* pending */
object Pending { val kind = SQL.Column.string("kind") val build_cluster = SQL.Column.bool("build_cluster") val hosts_spec = SQL.Column.string("hosts_spec") val timeout = SQL.Column.long("timeout") val other_settings = SQL.Column.string("other_settings") val uuid = SQL.Column.string("uuid").make_primary_key val submit_date = SQL.Column.date("submit_date") val priority = SQL.Column.string("priority") val isabelle_rev = SQL.Column.string("isabelle_rev") val extra_components = SQL.Column.string("extra_components")
val user = SQL.Column.string("user") val prefs = SQL.Column.string("prefs") val requirements = SQL.Column.bool("requirements") val all_sessions = SQL.Column.bool("all_sessions") val base_sessions = SQL.Column.string("base_sessions") val exclude_session_groups = SQL.Column.string("exclude_session_groups") val exclude_sessions = SQL.Column.string("exclude_sessions") val session_groups = SQL.Column.string("session_groups") val sessions = SQL.Column.string("sessions") val build_heap = SQL.Column.bool("build_heap") val clean_build = SQL.Column.bool("clean_build") val export_files = SQL.Column.bool("export_files") val fresh_build = SQL.Column.bool("fresh_build") val presentation = SQL.Column.bool("presentation") val verbose = SQL.Column.bool("verbose")
def pull_pending(db: SQL.Database): Build_Manager.State.Pending =
db.execute_query_statement(Pending.table.select(), Map.from[String, Task], get =
{ res => val kind = res.string(Pending.kind) val build_cluster = res.bool(Pending.build_cluster) val hosts_spec = res.string(Pending.hosts_spec) val timeout = Time.ms(res.long(Pending.timeout)) val other_settings = split_lines(res.string(Pending.other_settings)) val uuid = res.string(Pending.uuid) val submit_date = res.date(Pending.submit_date) val priority = Priority.valueOf(res.string(Pending.priority)) val isabelle_rev = res.string(Pending.isabelle_rev) val extra_components =
space_explode(',', res.string(Pending.extra_components)).map(Component.parse)
val build_config = if (kind != User_Build.name) CI_Build(kind, build_cluster, extra_components) else { val user = res.string(Pending.user) val prefs = Options.Spec.parse(res.string(Pending.prefs)) val requirements = res.bool(Pending.requirements) val all_sessions = res.bool(Pending.all_sessions) val base_sessions = space_explode(',', res.string(Pending.base_sessions)) val exclude_session_groups =
space_explode(',', res.string(Pending.exclude_session_groups)) val exclude_sessions = space_explode(',', res.string(Pending.exclude_sessions)) val session_groups = space_explode(',', res.string(Pending.session_groups)) val sessions = space_explode(',', res.string(Pending.sessions)) val build_heap = res.bool(Pending.build_heap) val clean_build = res.bool(Pending.clean_build) val export_files = res.bool(Pending.export_files) val fresh_build = res.bool(Pending.fresh_build) val presentation = res.bool(Pending.presentation) val verbose = res.bool(Pending.verbose)
object Running { val uuid = SQL.Column.string("uuid").make_primary_key val kind = SQL.Column.string("kind") val id = SQL.Column.long("id") val build_cluster = SQL.Column.bool("build_cluster") val hostnames = SQL.Column.string("hostnames") val components = SQL.Column.string("components") val timeout = SQL.Column.long("timeout") val user = SQL.Column.string("user") val start_date = SQL.Column.date("start_date") val cancelled = SQL.Column.bool("cancelled")
val table =
make_table(List(uuid, kind, id, build_cluster, hostnames, components, timeout, user,
start_date, cancelled),
name = "running")
}
def pull_running(db: SQL.Database): Build_Manager.State.Running =
db.execute_query_statement(Running.table.select(), Map.from[String, Job], get =
{ res => val uuid = res.string(Running.uuid) val kind = res.string(Running.kind) val id = res.long(Running.id) val build_cluster = res.bool(Running.build_cluster) val hostnames = space_explode(',', res.string(Running.hostnames)) val components = space_explode(',', res.string(Running.components)).map(Component.parse) val timeout = Time.ms(res.long(Running.timeout)) val user = res.get_string(Running.user) val start_date = res.date(Running.start_date) val cancelled = res.bool(Running.cancelled)
object Finished { val kind = SQL.Column.string("kind") val id = SQL.Column.long("id") val status = SQL.Column.string("status") val uuid = SQL.Column.string("uuid") val build_host = SQL.Column.string("build_host") val start_date = SQL.Column.date("start_date") val end_date = SQL.Column.date("end_date") val isabelle_version = SQL.Column.string("isabelle_version") val afp_version = SQL.Column.string("afp_version") val user = SQL.Column.string("user") val serial = SQL.Column.long("serial").make_primary_key
val table =
make_table(
List(kind, id, status, uuid, build_host, start_date, end_date, isabelle_version,
afp_version, user, serial),
name = "finished")
}
def read_finished_serial(db: SQL.Database): Long =
db.execute_query_statementO[Long](
Finished.table.select(List(Finished.serial.max)),
_.long(Finished.serial)).getOrElse(0L)
def pull_finished(
db: SQL.Database,
finished: Build_Manager.State.Finished
): Build_Manager.State.Finished = { val max_serial0 = Build_Manager.State.max_serial(finished.values.map(_.serial)) val max_serial1 = read_finished_serial(db) val missing = (max_serial0 + 1) to max_serial1
finished ++ db.execute_query_statement(
Finished.table.select(sql = Finished.serial.where_member_long(missing)),
Map.from[String, Result], get =
{ res => val kind = res.string(Finished.kind) val id = res.long(Finished.id) val status = Status.valueOf(res.string(Finished.status)) val uuid = res.get_string(Finished.uuid).map(UUID.make) val build_host = res.string(Finished.build_host) val start_date = res.date(Finished.start_date) val end_date = res.get_date(Finished.end_date) val isabelle_version = res.get_string(Finished.isabelle_version) val afp_version = res.get_string(Finished.afp_version) val user = res.get_string(Finished.user) val serial = res.long(Finished.serial)
val result = Result(kind, id, status, uuid, build_host, start_date, end_date,
isabelle_version, afp_version, user, serial)
result.name -> result
}
)
}
def push_finished(
db: SQL.Database,
finished: Build_Manager.State.Finished
): Build_Manager.State.Finished = { val (insert0, old) = finished.partition(_._2.serial == 0L) val max_serial = Build_Manager.State.max_serial(finished.map(_._2.serial)) val insert = for (((_, result), n) <- insert0.zipWithIndex) yield result.copy(serial = max_serial + 1 + n)
def write_log(
component: String,
repository: Mercurial.Repository,
rev0: String,
rev: String
): Unit = if (rev0.nonEmpty && rev.nonEmpty && rev0 != rev) { val log_opts = "--graph --color always" val rev1 = "children(" + rev0 + ")" val cmd = repository.command_line("log", Mercurial.opt_rev(rev1 + ":" + rev), log_opts) val log = Isabelle_System.bash(Bash.exports("HGPLAINEXCEPT=color") + cmd).check.out if (log.nonEmpty) File.write_gzip(dir + Path.basic(component).ext(log_ext).gz, log)
}
def write_diff(
component: String,
repository: Mercurial.Repository,
rev0: String,
rev: String
): Unit = if (rev0.nonEmpty && rev.nonEmpty) { val diff_opts = "--noprefix --nodates --ignore-all-space --color always" val cmd = repository.command_line("diff", Mercurial.opt_rev(rev0 + ":" + rev), diff_opts) val diff = Isabelle_System.bash(Bash.exports("HGPLAINEXCEPT=color") + cmd).check.out if (diff.nonEmpty) File.write_gzip(dir + Path.basic(component).ext(diff_ext).gz, diff)
}
def result(uuid: Option[UUID.T] = None, user: Option[String] = None): Result = { val End = """^Job ended at [^,]+, with status (\w+)$""".r
val build_log_file = Build_Log.Log_File(log_name, Library.trim_split_lines(read.build_log))
val meta_info = build_log_file.parse_meta_info() val status =
build_log_file.lines.last match { case End(status) => Status.valueOf(status) case _ => Status.aborted
} val build_host = meta_info.get_build_host.getOrElse(error("No build host")) val start_date = meta_info.get_build_start.getOrElse(error("No start date")) val end_date = meta_info.get_build_end val isabelle_version = meta_info.get(Build_Log.Prop.isabelle_version) val afp_version = meta_info.get(Build_Log.Prop.afp_version)
if (log_file.is_file) {
File.write_gzip(log_file_gz, build_log_file.text)
Isabelle_System.rm_tree(log_file)
}
def init(context: Context): State = { val process_future = Future.fork(Build_Process.open(context)) val result_future =
Future.fork(
process_future.join_result match { case Exn.Res(process) => process.run() case Exn.Exn(exn) => Process_Result(Process_Result.RC.interrupt).error(exn.getMessage)
})
privatedef maybe_result(name: String): Option[Process_Result] = for (future <- result_futures.get(name) if future.is_finished) yield
future.join_result match { case Exn.Res(result) => result case Exn.Exn(exn) => Process_Result(Process_Result.RC.interrupt).error(exn.getMessage)
}
privatedef do_terminate(name: String): Boolean = { val is_late = Time.now() > cancelling_until(name) if (is_late) process_futures(name).join.terminate()
is_late
}
def update: (State, Map[String, Process_Result]) = { val finished0 = for {
(name, _) <- result_futures
result <- maybe_result(name)
} yield name -> result
privatedef do_cancel(process_future: Future[Build_Process]): Boolean = { val is_finished = process_future.is_finished if (is_finished) process_future.join.cancel() else process_future.cancel()
is_finished
}
def cancel(cancelled: List[String]): State = { val cancelling_until1 = for {
name <- cancelled
process_future <- process_futures.get(name) if do_cancel(process_future)
} yield name -> (Time.now() + cancel_timeout)
val isabelle_rev = sync(isabelle_repository, task.isabelle_rev, context.task_dir)
val hostnames = task.build_hosts.map(_.hostname).distinct
val extra_components = for (extra_component <- task.extra_components) yield sync_dirs.find(_.name == extra_component.name) match { case Some(sync_dir) => val target = context.task_dir + sync_dir.target val rev = sync(sync_dir.hg, extra_component.rev, target)
if (!extra_component.is_local)
File.append(context.task_dir + Sync.DIRS_ROOTS, sync_dir.roots_entry + "\n")
extra_component.copy(rev = rev) case None => if (extra_component.is_local) extra_component else error("Unknown component " + extra_component)
}
if (task.kind != User_Build.name && _state.get_finished(task.kind).nonEmpty) { val previous = _state.get_finished(task.kind).maxBy(_.id)
privatedef stop_cancelled(state: Runner.State): Runner.State =
synchronized_database("stop_cancelled") { val now = Date.now() for {
name <- state.running
job = _state.running(name) if now - job.start_date > job.timeout
} {
_state = _state.cancel_running(name)
val timeout_msg = "Timeout after " + job.timeout.message_hms
store.report(job.kind, job.id).progress.echo_error_message(timeout_msg)
echo(job.name + ": " + timeout_msg)
}
val cancelled = for {
name <- state.running
job = _state.running(name) if job.cancelled
} yield job
privatedef finish_job(name: String, process_result: Process_Result): Unit =
synchronized_database("finish_job") { val job = _state.running(name)
val end_date = Date.now() val status = Status.from_result(process_result) val end_msg = "Job ended at " + Build_Log.print_date(end_date) + ", with status " + status
val report = store.report(job.kind, job.id)
report.progress.echo(end_msg)
def loop_body(state: Runner.State): Runner.State = { val state1 = if (progress.stopped) state else {
start_next() match { case None => state case Some(context) => state.init(context)
}
} val state2 = stop_cancelled(state1) val (state3, results) = state2.update
results.foreach(finish_job)
state3
}
}
val paths = Web_Server.paths(store) val cache = Cache.empty
enum Model { case Error extends Model case Cancelled extends Model case Home(state: State) extends Model case Overview(kind: String, state: State) extends Model case Details(build: Build, state: State, public: Boolean = true) extends Model case Diff(build: Build) extends Model
}
object View { import HTML.* import More_HTML.*
def render_if(cond: Boolean, body: XML.Body): XML.Body = if (cond) body else Nil
def waiting_for(host: Build_Cluster.Host): XML.Body =
build_hosts.find(_.hostname == host.hostname) match { case None => break ::: text(quote(host.hostname) + " is not a build host") case Some(host) => val active = state.running.values.filter(_.hostnames.contains(host.hostname)) if (active.isEmpty) Nil else break :::
text(host.hostname + " is busy with " + active.map(_.name).mkString(" and "))
}
if (num_before > 0) text("Waiting for " + num_before + " tasks to complete") else Exn.capture(task.build_hosts) match { case Exn.Res(hosts) => text("Hosts not ready:") ::: hosts.flatMap(waiting_for) case _ => text("Unkown host spec")
}
}
val lines = split_lines(s).map { case Colored(pre, code, s, post) => val codes = space_explode(';', code.stripSuffix(";1")).map(Value.Int.parse) val fg = codes match { case 0 :: i :: Nil => colors.unapply(i - 30) case _ => None }
val sp = span(if (code.endsWith(";1")) List(bold(text(s))) else text(s)) val sp1 = fg.map(color => sp + ("style" -> ("color:" + color))).getOrElse(sp)
List(span(text(pre)), sp1, span(text(post))) case line => text(Library.strip_ansi_color(line))
}
def render_diff(data: Report.Data, components: List[Component]): XML.Body =
par(List(page_link(Page.BUILD, "back to build", Markup.Name(build.name)))) ::
(for (component <- components if !component.is_local) yield { val infos =
data.component_logs.toMap.get(component.name).toList.flatMap(colored) :::
data.component_diffs.toMap.get(component.name).toList.flatMap(colored)
val header = if (infos.isEmpty) component.toString else component.name + ":"
par(subsubsection(header) :: infos)
})
build match { case job: Job =>
render_diff(cache.lookup(store.report(job.kind, job.id)), job.components) case result: Result =>
render_diff(cache.lookup(store.report(result.kind, result.id)), result.components) case _ => Nil
}
}
def get_overview(props: Properties.T): Option[Model.Overview] =
props match { case Markup.Kind(kind) => Some(Model.Overview(kind, _state)) case _ => None
}
def get_build(props: Properties.T): Option[Model.Details] =
props match { case Markup.Name(name) => val state = _state
state.get(name).map(Model.Details(_, state)) case Web_Server.Id(UUID(uuid)) => val state = _state
state.get(uuid).map(Model.Details(_, state, public = false)) case _ => None
}
def get_diff(props: Properties.T): Option[Model.Diff] =
props match { case Markup.Name(name) => _state.get(name).map(Model.Diff(_)) case _ => None
}
def cancel_build(params: Params.Data): Option[Model] = for {
uuid <- View.parse_uuid(params)
model <-
synchronized_database("cancel_build") {
_state.get(uuid).map { case task: Task =>
_state = _state.remove_pending(task.name)
Model.Cancelled case job: Job =>
_state = _state.cancel_running(job.name)
Model.Cancelled case result: Result => Model.Details(result, _state, public = false)
}
}
} yield model
def render(model: Model): XML.Body =
HTML.title("Isabelle Build Manager") :: (
model match { case Model.Error => HTML.text("invalid request") case Model.Home(state) => View.render_home(state) case Model.Overview(kind, state) => View.render_overview(kind, state) case Model.Details(build, state, public) => View.render_details(build, state, public) case Model.Diff(build) => View.render_diff(build) case Model.Cancelled => View.render_cancelled
})
val error_model: Model = Model.Error val endpoints = List(
Get(Page.HOME, "home", _ => overview),
Get(Page.OVERVIEW, "overview", get_overview),
Get(Page.BUILD, "build", get_build),
Get(Page.DIFF, "diff", get_diff),
Post(API.BUILD_CANCEL, "cancel build", cancel_build)) val logo = Bytes.read(Path.explode("$ISABELLE_HOME/lib/logo/isabelle_transparent-48.gif")) val head =
List(
HTML.title("Isabelle Build Manager"),
Web_App.More_HTML.icon("data:image/x-icon;base64," + logo.encode_base64.text),
HTML.style_file("https://hawkz.github.io/gdcss/gd.css"),
HTML.style("""
:root {
--color-secondary: var(--color-tertiary);
--color-secondary-hover: var(--color-tertiary-hover);
}
html { background-color: white; }"""))
}
overridedef close(): Unit = {
server.stop() super.close()
}
def init: Unit = server.start() def loop_body(u: Unit): Unit = if (!progress.stopped) synchronized_database("iterate") { cache.update() }
}
val processes = List( new Runner(store, build_hosts, isabelle_repository, sync_dirs, progress), new Poller(ci_jobs, store, isabelle_repository, sync_dirs, progress), new Timer(ci_jobs, store, progress), new Web_Server(port, store, build_hosts, progress))
val isabelle_tool = Isabelle_Tool("build_manager", "run build manager", Scala_Project.here,
{ args => var afp_root: Option[Path] = None val dirs = new mutable.ListBuffer[Path] val build_hosts = new mutable.ListBuffer[Build_Cluster.Host] var options = Options.init() var port = 0
val getopts = Getopts("""
Usage: isabelle build_manager [OPTIONS]
Options are:
-A ROOT include AFP with given root directory (":"for""" + AFP.BASE.implode + """)
-D DIR include extra component in given directory
-H HOSTS host specifications for all available hosts of the form
NAMES:PARAMETERS (separated by commas)
-o OPTION override Isabelle system OPTION (via NAME=VAL or NAME)
-p PORT explicit web server port
def build_manager_database(
options: Options,
sync_dirs: List[Sync.Dir] = Sync.afp_dirs(),
update_reports: Boolean = false,
progress: Progress = new Progress
): Unit = { val store = Store(options)
using(store.open_database()) { db =>
db.transaction { val tables0 = Build_Manager.private_data.tables.list val tables = tables0.filter(t => db.exists_table(t.name)) if (tables.nonEmpty) {
progress.echo("Removing tables " + commas_quote(tables.map(_.name)) + " ...")
db.execute_statement(SQL.MULTI(tables.map(db.destroy)))
}
}
val reports = for {
kind <- File.read_dir(store.finished)
entry <- File.read_dir(store.finished + Path.basic(kind))
id <- Value.Long.unapply(entry)
report = store.report(kind, id) if report.ok
} yield report
val results = reports.map(report => report -> report.result())
if (update_reports) { val isabelle_repository = Mercurial.self_repository() val afp_repository =
sync_dirs.find(_.name == Component.AFP).getOrElse(error("Missing AFP for udpate")).hg
isabelle_repository.pull()
afp_repository.pull()
for ((kind, results0) <- results.groupBy(_._1.kind) if kind != User_Build.name) { val results1 = results0.sortBy(_._1.id)
results1.foldLeft(("", "")) { case ((isabelle_rev0, afp_rev0), (report, result)) => val isabelle_rev = result.isabelle_version.getOrElse("") val afp_rev = result.afp_version.getOrElse("")
val isabelle_tool1 = Isabelle_Tool("build_manager_database", "restore build_manager database from log files",
Scala_Project.here,
{ args => var afp_root: Option[Path] = None var options = Options.init() var update_reports = false
val getopts = Getopts("""
Usage: isabelle build_manager_database [OPTIONS]
Options are:
-A ROOT include AFP with given root directory (":"for""" + AFP.BASE.implode + """)
-o OPTION override Isabelle system OPTION (via NAME=VAL or NAME)
-u update reports
val isabelle_tool2 = Isabelle_Tool("build_task", "submit build task for build manager",
Scala_Project.here,
{ args => var afp_root: Option[Path] = None val base_sessions = new mutable.ListBuffer[String] var presentation = false var requirements = false val exclude_session_groups = new mutable.ListBuffer[String] var all_sessions = false var build_heap = false var clean_build = false var export_files = false var fresh_build = false val session_groups = new mutable.ListBuffer[String] var options = Options.init(specs = Options.Spec.ISABELLE_BUILD_OPTIONS) val prefs = new mutable.ListBuffer[Options.Spec] var verbose = false var rev = "" val exclude_sessions = new mutable.ListBuffer[String]
val getopts = Getopts("""
Usage: isabelle build_task [OPTIONS] [SESSIONS ...]
Options are:
-A ROOT include AFP with given root directory (":"for""" + AFP.BASE.implode + """)
-B NAME include session NAME and all descendants
-P enable HTML/PDF presentation
-R refer to requirements of selected sessions
-X NAME exclude sessions from group NAME and all descendants
-a select all sessions
-b build heap images
-c clean build
-e export files from session specification into file-system
-f fresh build
-g NAME select session group NAME
-o OPTION override Isabelle system OPTION (via NAME=VAL or NAME)
-p OPTION override Isabelle system OPTION for build process
(via NAME=VAL or NAME)
-r REV explicit revision (default: state of working directory)
-v verbose
-x NAME exclude session NAME and all descendants
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.