/* Title: Pure/System/bash.scala Author: Makarius GNU bash processes, with propagation of interrupts. */ package isabelle import java.io.{File => JFile, BufferedReader, InputStreamReader, BufferedWriter, OutputStreamWriter} object Bash { /* concrete syntax */ private def bash_chr(c: Byte): String = { val ch = c.toChar ch match { case '\t' => "$'\\t'" case '\n' => "$'\\n'" case '\f' => "$'\\f'" case '\r' => "$'\\r'" case _ => if (Symbol.is_ascii_letter(ch) || Symbol.is_ascii_digit(ch) || "+,-./:_".contains(ch)) Symbol.ascii(ch) else if (c < 0) "$'\\x" + Integer.toHexString(256 + c) + "'" else if (c < 16) "$'\\x0" + Integer.toHexString(c) + "'" else if (c < 32 || c >= 127) "$'\\x" + Integer.toHexString(c) + "'" else "\\" + ch } } def string(s: String): String = if (s == "") "\"\"" else UTF8.bytes(s).iterator.map(bash_chr(_)).mkString def strings(ss: List[String]): String = ss.iterator.map(Bash.string(_)).mkString(" ") /* process and result */ private class Limited_Progress(proc: Process, progress_limit: Option[Long]) { private var count = 0L def apply(progress: String => Unit)(line: String): Unit = synchronized { progress(line) count = count + line.length + 1 progress_limit match { case Some(limit) if count > limit => proc.terminate case _ => } } } def process(script: String, cwd: JFile = null, env: Map[String, String] = Isabelle_System.settings(), redirect: Boolean = false, cleanup: () => Unit = () => ()): Process = new Process(script, cwd, env, redirect, cleanup) class Process private[Bash]( script: String, cwd: JFile, env: Map[String, String], redirect: Boolean, cleanup: () => Unit) { private val timing_file = Isabelle_System.tmp_file("bash_timing") private val timing = Synchronized[Option[Timing]](None) def get_timing: Timing = timing.value getOrElse Timing.zero private val script_file = Isabelle_System.tmp_file("bash_script") File.write(script_file, script) private val proc = Isabelle_System.process( List(File.platform_path(Path.variable("ISABELLE_BASH_PROCESS")), "-", File.standard_path(timing_file), "bash", File.standard_path(script_file)), cwd = cwd, env = env, redirect = redirect) // channels val stdin: BufferedWriter = new BufferedWriter(new OutputStreamWriter(proc.getOutputStream, UTF8.charset)) val stdout: BufferedReader = new BufferedReader(new InputStreamReader(proc.getInputStream, UTF8.charset)) val stderr: BufferedReader = new BufferedReader(new InputStreamReader(proc.getErrorStream, UTF8.charset)) // signals private val pid = stdout.readLine def interrupt() { Exn.Interrupt.postpone { Isabelle_System.kill("INT", pid) } } private def kill(signal: String): Boolean = Exn.Interrupt.postpone { Isabelle_System.kill(signal, pid) Isabelle_System.kill("0", pid)._2 == 0 } getOrElse true private def multi_kill(signal: String): Boolean = { var running = true var count = 10 while (running && count > 0) { if (kill(signal)) { Exn.Interrupt.postpone { Thread.sleep(100) count -= 1 } } else running = false } running } def terminate() { multi_kill("INT") && multi_kill("TERM") && kill("KILL") proc.destroy do_cleanup() } // JVM shutdown hook private val shutdown_hook = new Thread { override def run = terminate() } try { Runtime.getRuntime.addShutdownHook(shutdown_hook) } catch { case _: IllegalStateException => } // cleanup private def do_cleanup() { try { Runtime.getRuntime.removeShutdownHook(shutdown_hook) } catch { case _: IllegalStateException => } script_file.delete timing.change { case None => if (timing_file.isFile) { val t = Word.explode(File.read(timing_file)) match { case List(Value.Long(elapsed), Value.Long(cpu)) => Timing(Time.ms(elapsed), Time.ms(cpu), Time.zero) case _ => Timing.zero } timing_file.delete Some(t) } else Some(Timing.zero) case some => some } cleanup() } // join def join: Int = { val rc = proc.waitFor do_cleanup() rc } // result def result( progress_stdout: String => Unit = (_: String) => (), progress_stderr: String => Unit = (_: String) => (), progress_limit: Option[Long] = None, strict: Boolean = true): Process_Result = { stdin.close val limited = new Limited_Progress(this, progress_limit) val out_lines = Future.thread("bash_stdout") { File.read_lines(stdout, limited(progress_stdout)) } val err_lines = Future.thread("bash_stderr") { File.read_lines(stderr, limited(progress_stderr)) } val rc = try { join } catch { case Exn.Interrupt() => terminate(); Exn.Interrupt.return_code } if (strict && rc == Exn.Interrupt.return_code) throw Exn.Interrupt() Process_Result(rc, out_lines.join, err_lines.join, false, get_timing) } } }