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 = newmutable.ListBuffer[Path]
val build_hosts = newmutable.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
Run Isabelle build manager. """,
--> --------------------
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 ist noch experimentell.