| author | wenzelm | 
| Fri, 23 Aug 2024 20:21:04 +0200 | |
| changeset 80749 | 232a839ef8e6 | 
| parent 80480 | 972f7a4cdc0e | 
| child 82465 | 3cc075052033 | 
| permissions | -rw-r--r-- | 
| 62584 | 1 | /* Title: Pure/System/bash.scala | 
| 60991 | 2 | Author: Makarius | 
| 3 | ||
| 74158 | 4 | Support for GNU bash: portable external processes with propagation of | 
| 5 | interrupts. | |
| 60991 | 6 | */ | 
| 7 | ||
| 8 | package isabelle | |
| 9 | ||
| 10 | ||
| 80218 | 11 | import java.util.{List => JList, Map => JMap, LinkedList}
 | 
| 74273 
0a4cdf0036a0
more robust: client could have terminated already;
 wenzelm parents: 
74212diff
changeset | 12 | import java.io.{BufferedReader, BufferedWriter, InputStreamReader, OutputStreamWriter,
 | 
| 
0a4cdf0036a0
more robust: client could have terminated already;
 wenzelm parents: 
74212diff
changeset | 13 | File => JFile, IOException} | 
| 71706 | 14 | import scala.annotation.tailrec | 
| 73602 
37243ad3ecb6
fast approximation of test for process group (NB: initial process might already be terminated, while background processes are still running);
 wenzelm parents: 
73601diff
changeset | 15 | import scala.jdk.OptionConverters._ | 
| 71706 | 16 | |
| 60991 | 17 | |
| 75393 | 18 | object Bash {
 | 
| 64304 | 19 | /* concrete syntax */ | 
| 20 | ||
| 75393 | 21 |   private def bash_chr(c: Byte): String = {
 | 
| 64304 | 22 | val ch = c.toChar | 
| 23 |     ch match {
 | |
| 24 | case '\t' => "$'\\t'" | |
| 25 | case '\n' => "$'\\n'" | |
| 26 | case '\f' => "$'\\f'" | |
| 27 | case '\r' => "$'\\r'" | |
| 28 | case _ => | |
| 67200 | 29 | if (Symbol.is_ascii_letter(ch) || Symbol.is_ascii_digit(ch) || "+,-./:_".contains(ch)) | 
| 64304 | 30 | Symbol.ascii(ch) | 
| 31 | else if (c < 0) "$'\\x" + Integer.toHexString(256 + c) + "'" | |
| 32 | else if (c < 16) "$'\\x0" + Integer.toHexString(c) + "'" | |
| 33 | else if (c < 32 || c >= 127) "$'\\x" + Integer.toHexString(c) + "'" | |
| 34 | else "\\" + ch | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | def string(s: String): String = | |
| 80354 | 39 | if (s.isEmpty) "\"\"" | 
| 71601 | 40 | else UTF8.bytes(s).iterator.map(bash_chr).mkString | 
| 64304 | 41 | |
| 78910 | 42 | def strings(ss: Iterable[String]): String = | 
| 71601 | 43 |     ss.iterator.map(Bash.string).mkString(" ")
 | 
| 64304 | 44 | |
| 45 | ||
| 46 | /* process and result */ | |
| 47 | ||
| 80227 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 48 | def context(script: String, | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 49 | user_home: String = "", | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 50 | isabelle_identifier: String = "", | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 51 | cwd: Path = Path.current | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 52 |   ): String = {
 | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 53 | if_proper(user_home, | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 54 | "export USER_HOME=" + Bash.string(user_home) + "\n") + | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 55 | if_proper(isabelle_identifier, | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 56 | "export ISABELLE_IDENTIFIER=" + Bash.string(isabelle_identifier) + "\n") + | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 57 | (if (cwd == null || cwd.is_current) "" else "cd " + quote(cwd.implode) + "\n") + | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 58 | script | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 59 | } | 
| 
af6b60c75d7d
clarified context for (remote) bash scripts: export variables are optional, support cwd;
 wenzelm parents: 
80224diff
changeset | 60 | |
| 74212 | 61 | private def make_description(description: String): String = | 
| 62 | proper_string(description) getOrElse "bash_process" | |
| 63 | ||
| 80218 | 64 | def local_bash_process(): String = | 
| 65 |     File.platform_path(Path.variable("ISABELLE_BASH_PROCESS"))
 | |
| 66 | ||
| 67 | def local_bash(): String = | |
| 68 | if (Platform.is_unix) "bash" | |
| 69 | else isabelle.setup.Environment.cygwin_root() + "\\bin\\bash.exe" | |
| 70 | ||
| 71 |   def remote_bash_process(ssh: SSH.Session): String = {
 | |
| 72 | val component = Components.provide(Component_Bash_Process.home, ssh = ssh) | |
| 73 | val exe = Component_Bash_Process.remote_program(component) | |
| 74 | ssh.make_command(args_host = true, args = ssh.bash_path(exe)) | |
| 75 | } | |
| 76 | ||
| 80235 | 77 |   object Watchdog {
 | 
| 78 |     val none: Watchdog = new Watchdog(Time.zero, _ => false) {
 | |
| 79 | override def toString: String = "Bash.Watchdog.none" | |
| 80 | } | |
| 81 | def apply(time: Time, check: Process => Boolean = _ => true): Watchdog = | |
| 82 | if (time.is_zero) none else new Watchdog(time, check) | |
| 83 | } | |
| 84 |   class Watchdog private(val time: Time, val check: Process => Boolean) {
 | |
| 85 |     override def toString: String = "Bash.Watchdog(" + time + ")"
 | |
| 80237 
305d2f4a395f
more robust: avoid crash of sleep() for negative time;
 wenzelm parents: 
80235diff
changeset | 86 | def defined: Boolean = time > Time.zero | 
| 80235 | 87 | } | 
| 72598 | 88 | |
| 62545 
8ebffdaf2ce2
Bash.process always uses a closed script instead of an open argument list, for extra robustness on Windows, where quoting is not well-defined;
 wenzelm parents: 
62543diff
changeset | 89 | def process(script: String, | 
| 74212 | 90 | description: String = "", | 
| 80218 | 91 | ssh: SSH.System = SSH.Local, | 
| 80224 
db92e0b6a11a
clarified signature: prefer symbolic isabelle.Path over physical java.io.File;
 wenzelm parents: 
80221diff
changeset | 92 | cwd: Path = Path.current, | 
| 80229 | 93 | env: JMap[String, String] = Isabelle_System.settings(), // ignored for remote ssh | 
| 62602 | 94 | redirect: Boolean = false, | 
| 95 | cleanup: () => Unit = () => ()): Process = | |
| 80218 | 96 | new Process(script, description, ssh, cwd, env, redirect, cleanup) | 
| 60991 | 97 | |
| 63996 | 98 | class Process private[Bash]( | 
| 75393 | 99 | script: String, | 
| 100 | description: String, | |
| 80218 | 101 | ssh: SSH.System, | 
| 80224 
db92e0b6a11a
clarified signature: prefer symbolic isabelle.Path over physical java.io.File;
 wenzelm parents: 
80221diff
changeset | 102 | cwd: Path, | 
| 75393 | 103 | env: JMap[String, String], | 
| 104 | redirect: Boolean, | |
| 105 | cleanup: () => Unit | |
| 106 |   ) {
 | |
| 74212 | 107 | override def toString: String = make_description(description) | 
| 108 | ||
| 80218 | 109 | private val proc_command: JList[String] = new LinkedList[String] | 
| 110 | ||
| 111 | private val winpid_file: Option[JFile] = | |
| 112 |       if (ssh.is_local && Platform.is_windows) Some(Isabelle_System.tmp_file("bash_winpid"))
 | |
| 113 | else None | |
| 114 | private val winpid_script = | |
| 115 |       winpid_file match {
 | |
| 116 | case None => "" | |
| 117 | case Some(file) => | |
| 118 | "read < /proc/self/winpid > /dev/null 2> /dev/null\n" + | |
| 119 | """echo -n "$REPLY" > """ + File.bash_path(file) + "\n\n" | |
| 120 | } | |
| 121 | ||
| 80220 
928e02d0cab7
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80218diff
changeset | 122 | private val List(timing_file, script_file) = | 
| 
928e02d0cab7
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80218diff
changeset | 123 |       ssh.tmp_files(List("bash_timing", "bash_script"))
 | 
| 
928e02d0cab7
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80218diff
changeset | 124 | |
| 62569 | 125 | private val timing = Synchronized[Option[Timing]](None) | 
| 65317 | 126 | def get_timing: Timing = timing.value getOrElse Timing.zero | 
| 62569 | 127 | |
| 80228 | 128 | ssh.write(script_file, | 
| 129 | if (ssh.is_local) winpid_script + script | |
| 130 | else Bash.context(script, cwd = cwd)) | |
| 80218 | 131 | |
| 132 | private val ssh_file: Option[JFile] = | |
| 133 |       ssh.ssh_session match {
 | |
| 134 | case None => | |
| 135 | proc_command.add(local_bash_process()) | |
| 136 | proc_command.add(File.platform_path(timing_file)) | |
| 137 |           proc_command.add("bash")
 | |
| 138 | proc_command.add(File.platform_path(script_file)) | |
| 139 | None | |
| 140 | case Some(ssh_session) => | |
| 141 | val ssh_script = | |
| 142 | "exec " + remote_bash_process(ssh_session) + " " + | |
| 143 | ssh.bash_path(timing_file) + " bash " + | |
| 144 | ssh.bash_path(script_file) | |
| 145 |           val file = Isabelle_System.tmp_file("bash_ssh")
 | |
| 146 | File.write(file, ssh_script) | |
| 147 | proc_command.add(local_bash()) | |
| 148 | proc_command.add(file.getPath) | |
| 149 | Some(file) | |
| 73603 
342362c9496c
clarified check of root process on Windows (NB: the winpid is less stable than the Cygwin/Posix pid, so it needs to be "patched" into the the bash script, instead of bash_process.c);
 wenzelm parents: 
73602diff
changeset | 150 | } | 
| 
342362c9496c
clarified check of root process on Windows (NB: the winpid is less stable than the Cygwin/Posix pid, so it needs to be "patched" into the the bash script, instead of bash_process.c);
 wenzelm parents: 
73602diff
changeset | 151 | |
| 62296 | 152 | private val proc = | 
| 80224 
db92e0b6a11a
clarified signature: prefer symbolic isabelle.Path over physical java.io.File;
 wenzelm parents: 
80221diff
changeset | 153 | isabelle.setup.Environment.process_builder( | 
| 
db92e0b6a11a
clarified signature: prefer symbolic isabelle.Path over physical java.io.File;
 wenzelm parents: 
80221diff
changeset | 154 | proc_command, | 
| 80228 | 155 | if (!ssh.is_local || cwd == null || cwd.is_current) null else cwd.file, | 
| 80224 
db92e0b6a11a
clarified signature: prefer symbolic isabelle.Path over physical java.io.File;
 wenzelm parents: 
80221diff
changeset | 156 | env, | 
| 
db92e0b6a11a
clarified signature: prefer symbolic isabelle.Path over physical java.io.File;
 wenzelm parents: 
80221diff
changeset | 157 | redirect | 
| 
db92e0b6a11a
clarified signature: prefer symbolic isabelle.Path over physical java.io.File;
 wenzelm parents: 
80221diff
changeset | 158 | ).start() | 
| 60991 | 159 | |
| 160 | ||
| 161 | // channels | |
| 162 | ||
| 163 | val stdin: BufferedWriter = | |
| 164 | new BufferedWriter(new OutputStreamWriter(proc.getOutputStream, UTF8.charset)) | |
| 165 | ||
| 166 | val stdout: BufferedReader = | |
| 167 | new BufferedReader(new InputStreamReader(proc.getInputStream, UTF8.charset)) | |
| 168 | ||
| 169 | val stderr: BufferedReader = | |
| 170 | new BufferedReader(new InputStreamReader(proc.getErrorStream, UTF8.charset)) | |
| 171 | ||
| 172 | ||
| 173 | // signals | |
| 174 | ||
| 73601 | 175 | private val group_pid = stdout.readLine | 
| 60991 | 176 | |
| 80218 | 177 | private def local_process_alive(pid: String): Boolean = | 
| 178 | ssh.is_local && | |
| 179 |         (for {
 | |
| 180 | p <- Value.Long.unapply(pid) | |
| 181 | handle <- ProcessHandle.of(p).toScala | |
| 182 | } yield handle.isAlive).getOrElse(false) | |
| 73603 
342362c9496c
clarified check of root process on Windows (NB: the winpid is less stable than the Cygwin/Posix pid, so it needs to be "patched" into the the bash script, instead of bash_process.c);
 wenzelm parents: 
73602diff
changeset | 183 | |
| 
342362c9496c
clarified check of root process on Windows (NB: the winpid is less stable than the Cygwin/Posix pid, so it needs to be "patched" into the the bash script, instead of bash_process.c);
 wenzelm parents: 
73602diff
changeset | 184 | private def root_process_alive(): Boolean = | 
| 
342362c9496c
clarified check of root process on Windows (NB: the winpid is less stable than the Cygwin/Posix pid, so it needs to be "patched" into the the bash script, instead of bash_process.c);
 wenzelm parents: 
73602diff
changeset | 185 |       winpid_file match {
 | 
| 80218 | 186 | case None => local_process_alive(group_pid) | 
| 73603 
342362c9496c
clarified check of root process on Windows (NB: the winpid is less stable than the Cygwin/Posix pid, so it needs to be "patched" into the the bash script, instead of bash_process.c);
 wenzelm parents: 
73602diff
changeset | 187 | case Some(file) => | 
| 80218 | 188 | file.exists() && local_process_alive(Library.trim_line(File.read(file))) | 
| 73603 
342362c9496c
clarified check of root process on Windows (NB: the winpid is less stable than the Cygwin/Posix pid, so it needs to be "patched" into the the bash script, instead of bash_process.c);
 wenzelm parents: 
73602diff
changeset | 189 | } | 
| 73602 
37243ad3ecb6
fast approximation of test for process group (NB: initial process might already be terminated, while background processes are still running);
 wenzelm parents: 
73601diff
changeset | 190 | |
| 75393 | 191 |     @tailrec private def signal(s: String, count: Int = 1): Boolean = {
 | 
| 192 |       count <= 0 || {
 | |
| 80218 | 193 | ssh.kill_process(group_pid, s) | 
| 194 | val running = root_process_alive() || ssh.kill_process(group_pid, "0") | |
| 71707 | 195 |         if (running) {
 | 
| 80218 | 196 | Time.seconds(if (ssh.is_local) 0.1 else 0.25).sleep() | 
| 73601 | 197 | signal(s, count - 1) | 
| 60991 | 198 | } | 
| 71706 | 199 | else false | 
| 60991 | 200 | } | 
| 201 | } | |
| 202 | ||
| 75393 | 203 |     def terminate(): Unit = Isabelle_Thread.try_uninterruptible {
 | 
| 73601 | 204 |       signal("INT", count = 7) && signal("TERM", count = 3) && signal("KILL")
 | 
| 73367 | 205 | proc.destroy() | 
| 62602 | 206 | do_cleanup() | 
| 62574 | 207 | } | 
| 60991 | 208 | |
| 75393 | 209 |     def interrupt(): Unit = Isabelle_Thread.try_uninterruptible {
 | 
| 80218 | 210 | ssh.kill_process(group_pid, "INT") | 
| 71705 | 211 | } | 
| 212 | ||
| 60991 | 213 | |
| 62574 | 214 | // cleanup | 
| 62545 
8ebffdaf2ce2
Bash.process always uses a closed script instead of an open argument list, for extra robustness on Windows, where quoting is not well-defined;
 wenzelm parents: 
62543diff
changeset | 215 | |
| 76146 | 216 | private val shutdown_hook = | 
| 217 |       Isabelle_System.create_shutdown_hook { terminate() }
 | |
| 218 | ||
| 75393 | 219 |     private def do_cleanup(): Unit = {
 | 
| 76146 | 220 | Isabelle_System.remove_shutdown_hook(shutdown_hook) | 
| 60991 | 221 | |
| 73603 
342362c9496c
clarified check of root process on Windows (NB: the winpid is less stable than the Cygwin/Posix pid, so it needs to be "patched" into the the bash script, instead of bash_process.c);
 wenzelm parents: 
73602diff
changeset | 222 | winpid_file.foreach(_.delete()) | 
| 80218 | 223 | ssh_file.foreach(_.delete()) | 
| 224 | ||
| 62569 | 225 |       timing.change {
 | 
| 226 | case None => | |
| 80221 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 227 | val timing_text = | 
| 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 228 |             try { ssh.read(timing_file) }
 | 
| 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 229 |             catch { case ERROR(_) => "" }
 | 
| 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 230 | val t = | 
| 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 231 |             Word.explode(timing_text) match {
 | 
| 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 232 | case List(Value.Long(elapsed), Value.Long(cpu)) => | 
| 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 233 | Timing(Time.ms(elapsed), Time.ms(cpu), Time.zero) | 
| 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 234 | case _ => Timing.zero | 
| 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 235 | } | 
| 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 236 | Some(t) | 
| 62569 | 237 | case some => some | 
| 238 | } | |
| 62602 | 239 | |
| 80221 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 240 | ssh.delete(timing_file, script_file) | 
| 
0d89f0a39854
minor performance tuning: save approx. 70ms per SSH command;
 wenzelm parents: 
80220diff
changeset | 241 | |
| 62602 | 242 | cleanup() | 
| 62574 | 243 | } | 
| 62569 | 244 | |
| 62574 | 245 | |
| 246 | // join | |
| 247 | ||
| 75393 | 248 |     def join(): Int = {
 | 
| 74141 | 249 | val rc = proc.waitFor() | 
| 62602 | 250 | do_cleanup() | 
| 62545 
8ebffdaf2ce2
Bash.process always uses a closed script instead of an open argument list, for extra robustness on Windows, where quoting is not well-defined;
 wenzelm parents: 
62543diff
changeset | 251 | rc | 
| 
8ebffdaf2ce2
Bash.process always uses a closed script instead of an open argument list, for extra robustness on Windows, where quoting is not well-defined;
 wenzelm parents: 
62543diff
changeset | 252 | } | 
| 62543 | 253 | |
| 254 | ||
| 62574 | 255 | // result | 
| 62543 | 256 | |
| 257 | def result( | |
| 74142 
0f051404f487
clarified signature: more options for bash_process;
 wenzelm parents: 
74141diff
changeset | 258 | input: String = "", | 
| 62543 | 259 | progress_stdout: String => Unit = (_: String) => (), | 
| 260 | progress_stderr: String => Unit = (_: String) => (), | |
| 80235 | 261 | watchdog: Watchdog = Watchdog.none, | 
| 75393 | 262 | strict: Boolean = true | 
| 263 |     ): Process_Result = {
 | |
| 74142 
0f051404f487
clarified signature: more options for bash_process;
 wenzelm parents: 
74141diff
changeset | 264 | val in = | 
| 
0f051404f487
clarified signature: more options for bash_process;
 wenzelm parents: 
74141diff
changeset | 265 | if (input.isEmpty) Future.value(stdin.close()) | 
| 
0f051404f487
clarified signature: more options for bash_process;
 wenzelm parents: 
74141diff
changeset | 266 |         else Future.thread("bash_stdin") { stdin.write(input); stdin.flush(); stdin.close(); }
 | 
| 62543 | 267 | val out_lines = | 
| 72105 | 268 |         Future.thread("bash_stdout") { File.read_lines(stdout, progress_stdout) }
 | 
| 62543 | 269 | val err_lines = | 
| 72105 | 270 |         Future.thread("bash_stderr") { File.read_lines(stderr, progress_stderr) }
 | 
| 62543 | 271 | |
| 72598 | 272 | val watchdog_thread = | 
| 80235 | 273 |         if (watchdog.defined) {
 | 
| 274 | Some( | |
| 275 |             Future.thread("bash_watchdog") {
 | |
| 276 |               while (proc.isAlive) {
 | |
| 277 | watchdog.time.sleep() | |
| 278 | if (watchdog.check(this)) interrupt() | |
| 279 | } | |
| 280 | }) | |
| 72598 | 281 | } | 
| 80235 | 282 | else None | 
| 72598 | 283 | |
| 62543 | 284 | val rc = | 
| 74141 | 285 |         try { join() }
 | 
| 74306 | 286 |         catch { case Exn.Interrupt() => terminate(); Process_Result.RC.interrupt }
 | 
| 72598 | 287 | |
| 73367 | 288 | watchdog_thread.foreach(_.cancel()) | 
| 72598 | 289 | |
| 74142 
0f051404f487
clarified signature: more options for bash_process;
 wenzelm parents: 
74141diff
changeset | 290 | in.join | 
| 
0f051404f487
clarified signature: more options for bash_process;
 wenzelm parents: 
74141diff
changeset | 291 | out_lines.join | 
| 
0f051404f487
clarified signature: more options for bash_process;
 wenzelm parents: 
74141diff
changeset | 292 | err_lines.join | 
| 
0f051404f487
clarified signature: more options for bash_process;
 wenzelm parents: 
74141diff
changeset | 293 | |
| 74306 | 294 | if (strict && rc == Process_Result.RC.interrupt) throw Exn.Interrupt() | 
| 62543 | 295 | |
| 73134 
8a8ffe78eee7
clarified return code: re-use SIGALRM for soft timeout;
 wenzelm parents: 
72598diff
changeset | 296 | Process_Result(rc, out_lines.join, err_lines.join, get_timing) | 
| 62543 | 297 | } | 
| 60991 | 298 | } | 
| 73228 
0575cfd2ecfc
support multi-threaded Bash.process invocation on Apple Silicon, where Poly/ML is running on Rosetta 2;
 wenzelm parents: 
73134diff
changeset | 299 | |
| 
0575cfd2ecfc
support multi-threaded Bash.process invocation on Apple Silicon, where Poly/ML is running on Rosetta 2;
 wenzelm parents: 
73134diff
changeset | 300 | |
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 301 | /* server */ | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 302 | |
| 75393 | 303 |   object Server {
 | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 304 | // input messages | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 305 | private val RUN = "run" | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 306 | private val KILL = "kill" | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 307 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 308 | // output messages | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 309 | private val UUID = "uuid" | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 310 | private val INTERRUPT = "interrupt" | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 311 | private val FAILURE = "failure" | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 312 | private val RESULT = "result" | 
| 73228 
0575cfd2ecfc
support multi-threaded Bash.process invocation on Apple Silicon, where Poly/ML is running on Rosetta 2;
 wenzelm parents: 
73134diff
changeset | 313 | |
| 75393 | 314 |     def start(port: Int = 0, debugging: => Boolean = false): Server = {
 | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 315 | val server = new Server(port, debugging) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 316 | server.start() | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 317 | server | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 318 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 319 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 320 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 321 | class Server private(port: Int, debugging: => Boolean) | 
| 75393 | 322 |   extends isabelle.Server.Handler(port) {
 | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 323 | server => | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 324 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 325 | private val _processes = Synchronized(Map.empty[UUID.T, Bash.Process]) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 326 | |
| 75393 | 327 |     override def stop(): Unit = {
 | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 328 | for ((_, process) <- _processes.value) process.terminate() | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 329 | super.stop() | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 330 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 331 | |
| 75393 | 332 |     override def handle(connection: isabelle.Server.Connection): Unit = {
 | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 333 | def reply(chunks: List[String]): Unit = | 
| 74273 
0a4cdf0036a0
more robust: client could have terminated already;
 wenzelm parents: 
74212diff
changeset | 334 |         try { connection.write_byte_message(chunks.map(Bytes.apply)) }
 | 
| 
0a4cdf0036a0
more robust: client could have terminated already;
 wenzelm parents: 
74212diff
changeset | 335 |         catch { case _: IOException => }
 | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 336 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 337 | def reply_failure(exn: Throwable): Unit = | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 338 | reply( | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 339 | if (Exn.is_interrupt(exn)) List(Server.INTERRUPT) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 340 | else List(Server.FAILURE, Exn.message(exn))) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 341 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 342 | def reply_result(result: Process_Result): Unit = | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 343 | reply( | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 344 | Server.RESULT :: | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 345 | result.rc.toString :: | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 346 | result.timing.elapsed.ms.toString :: | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 347 | result.timing.cpu.ms.toString :: | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 348 | result.out_lines.length.toString :: | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 349 | result.out_lines ::: | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 350 | result.err_lines) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 351 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 352 |       connection.read_byte_message().map(_.map(_.text)) match {
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 353 | case None => | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 354 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 355 | case Some(List(Server.KILL, UUID(uuid))) => | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 356 |           if (debugging) Output.writeln("kill " + uuid)
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 357 | _processes.value.get(uuid).foreach(_.terminate()) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 358 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 359 | case Some(List(Server.RUN, script, input, cwd, putenv, | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 360 | Value.Boolean(redirect), Value.Seconds(timeout), description)) => | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 361 | val uuid = UUID.random() | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 362 | |
| 74212 | 363 | val descr = make_description(description) | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 364 |           if (debugging) {
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 365 | Output.writeln( | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 366 | "start " + quote(descr) + " (uuid=" + uuid + ", timeout=" + timeout.seconds + ")") | 
| 74142 
0f051404f487
clarified signature: more options for bash_process;
 wenzelm parents: 
74141diff
changeset | 367 | } | 
| 73228 
0575cfd2ecfc
support multi-threaded Bash.process invocation on Apple Silicon, where Poly/ML is running on Rosetta 2;
 wenzelm parents: 
73134diff
changeset | 368 | |
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 369 |           Exn.capture {
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 370 | Bash.process(script, | 
| 74212 | 371 | description = description, | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 372 | cwd = | 
| 80480 
972f7a4cdc0e
clarified YXML.Source: more direct support for String and Bytes, instead of CharSequence;
 wenzelm parents: 
80354diff
changeset | 373 |                 XML.Decode.option(XML.Decode.string)(YXML.parse_body(YXML.Source(cwd))) match {
 | 
| 80224 
db92e0b6a11a
clarified signature: prefer symbolic isabelle.Path over physical java.io.File;
 wenzelm parents: 
80221diff
changeset | 374 | case None => Path.current | 
| 
db92e0b6a11a
clarified signature: prefer symbolic isabelle.Path over physical java.io.File;
 wenzelm parents: 
80221diff
changeset | 375 | case Some(s) => Path.explode(s) | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 376 | }, | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 377 | env = | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 378 | Isabelle_System.settings( | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 379 | XML.Decode.list(XML.Decode.pair(XML.Decode.string, XML.Decode.string))( | 
| 80480 
972f7a4cdc0e
clarified YXML.Source: more direct support for String and Bytes, instead of CharSequence;
 wenzelm parents: 
80354diff
changeset | 380 | YXML.parse_body(YXML.Source(putenv)))), | 
| 80271 | 381 | redirect = redirect) | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 382 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 383 |           match {
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 384 | case Exn.Exn(exn) => reply_failure(exn) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 385 | case Exn.Res(process) => | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 386 | _processes.change(processes => processes + (uuid -> process)) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 387 | reply(List(Server.UUID, uuid.toString)) | 
| 73228 
0575cfd2ecfc
support multi-threaded Bash.process invocation on Apple Silicon, where Poly/ML is running on Rosetta 2;
 wenzelm parents: 
73134diff
changeset | 388 | |
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 389 |               Isabelle_Thread.fork(name = "bash_process") {
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 390 | @volatile var is_timeout = false | 
| 80235 | 391 | val watchdog = | 
| 392 | if (timeout.is_zero) Watchdog.none | |
| 393 |                   else Watchdog(timeout, _ => { is_timeout = true; true })
 | |
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 394 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 395 |                 Exn.capture { process.result(input = input, watchdog = watchdog, strict = false) }
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 396 |                 match {
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 397 | case Exn.Exn(exn) => reply_failure(exn) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 398 | case Exn.Res(res0) => | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 399 | val res = if (!res0.ok && is_timeout) res0.timeout_rc else res0 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 400 |                     if (debugging) {
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 401 | Output.writeln( | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 402 | "stop " + quote(descr) + " (uuid=" + uuid + ", return_code=" + res.rc + ")") | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 403 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 404 | reply_result(res) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 405 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 406 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 407 | _processes.change(provers => provers - uuid) | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 408 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 409 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 410 | connection.await_close() | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 411 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 412 | |
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 413 |         case Some(_) => reply_failure(ERROR("Bad protocol message"))
 | 
| 73228 
0575cfd2ecfc
support multi-threaded Bash.process invocation on Apple Silicon, where Poly/ML is running on Rosetta 2;
 wenzelm parents: 
73134diff
changeset | 414 | } | 
| 
0575cfd2ecfc
support multi-threaded Bash.process invocation on Apple Silicon, where Poly/ML is running on Rosetta 2;
 wenzelm parents: 
73134diff
changeset | 415 | } | 
| 
0575cfd2ecfc
support multi-threaded Bash.process invocation on Apple Silicon, where Poly/ML is running on Rosetta 2;
 wenzelm parents: 
73134diff
changeset | 416 | } | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 417 | |
| 75393 | 418 |   class Handler extends Session.Protocol_Handler {
 | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 419 | private var server: Server = null | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 420 | |
| 75393 | 421 |     override def init(session: Session): Unit = {
 | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 422 | exit() | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 423 |       server = Server.start(debugging = session.session_options.bool("bash_process_debugging"))
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 424 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 425 | |
| 75393 | 426 |     override def exit(): Unit = {
 | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 427 |       if (server != null) {
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 428 | server.stop() | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 429 | server = null | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 430 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 431 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 432 | |
| 75393 | 433 |     override def prover_options(options: Options): Options = {
 | 
| 74147 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 434 | val address = if (server == null) "" else server.address | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 435 | val password = if (server == null) "" else server.password | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 436 | options + | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 437 |         ("bash_process_address=" + address) +
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 438 |         ("bash_process_password=" + password)
 | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 439 | } | 
| 
d030b988d470
provide bash_process server for Isabelle/ML and other external programs;
 wenzelm parents: 
74142diff
changeset | 440 | } | 
| 60991 | 441 | } |