// TLCockpit // Copyright 2017-2018 Norbert Preining // Licensed according to GPLv3+ // // Front end for tlmgr package TLCockpit import javafx.scene.Node import TLCockpit.Utils._ import TeXLive._ import TeXLive.OsTools._ import scala.collection.{immutable, mutable} import scala.collection.mutable.ArrayBuffer import scala.concurrent.{Future, Promise, SyncVar} import scala.concurrent.ExecutionContext.Implicits.global import scala.io.Source import scala.util.{Failure, Success} import scala.sys.process._ import scalafx.beans.property.BooleanProperty import scalafx.scene.text.Font // ScalaFX imports import scalafx.event.Event import scalafx.beans.property.{ObjectProperty, StringProperty} import scalafx.geometry.{Pos, Orientation} import scalafx.scene.Cursor import scalafx.scene.control.Alert.AlertType import scalafx.scene.image.{Image, ImageView} import scalafx.scene.input.{KeyCode, KeyEvent, MouseEvent} import scalafx.scene.paint.Color // needed see https://github.com/scalafx/scalafx/issues/137 import scalafx.scene.control.TableColumn._ import scalafx.scene.control.TreeTableColumn._ import scalafx.scene.control.TreeItem import scalafx.scene.control.Menu._ import scalafx.scene.control.ListCell import scalafx.Includes._ import scalafx.application.{JFXApp, Platform} import scalafx.application.JFXApp.PrimaryStage import scalafx.geometry.Insets import scalafx.scene.Scene import scalafx.scene.layout._ import scalafx.scene.control._ import scalafx.event.ActionEvent import scalafx.collections.ObservableBuffer import scalafx.collections.ObservableMap // configuration file handling support import java.util.Properties import java.io.{ File, FileOutputStream, FileInputStream } // JSON support - important load TLPackageJsonProtocol later! import spray.json._ import TeXLive.JsonProtocol._ import org.json4s._ import org.json4s.jackson.JsonMethods._ // logging import com.typesafe.scalalogging.LazyLogging import ch.qos.logback.classic.{Level,Logger} import org.slf4j.LoggerFactory object ApplicationMain extends JFXApp with LazyLogging { val version: String = getClass.getPackage.getImplementationVersion // parse command line arguments // nothing => INFO // -q WARN -qq ERROR // -d => DEBUG -dd => TRACE val cmdlnlog: Int = parameters.unnamed.map( { case "-d" => Level.DEBUG_INT case "-dd" => Level.TRACE_INT case "-q" => Level.WARN_INT case "-qq" => Level.ERROR_INT case _ => -1 } ).foldLeft(Level.OFF_INT)(scala.math.min) if (cmdlnlog == -1) { // Unknown log level has been passed in, error out Console.err.println("Unsupported command line argument passed in, terminating.") Platform.exit() sys.exit(0) } // if nothing has been passed on the command line, use INFO val newloglevel: Int = if (cmdlnlog == Level.OFF_INT) Level.INFO_INT else cmdlnlog LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME). asInstanceOf[Logger].setLevel(Level.toLevel(newloglevel)) logger.trace("starting program tlcockpit") val javaVersion = System.getProperty("java.specification.version") val javaVersionSplit: Array[String] = javaVersion.split('.') logger.debug(s"Got javaVersion ${javaVersion}") val major = toInt(javaVersionSplit(0)) major match { case Some(i) => if (major.get == 1) { val minor = toInt(javaVersionSplit(1)) minor match { case Some(j) => if (minor.get < 8) { logger.error(s"Java version ${javaVersion} too old, need >= 1.8, terminating!") Platform.exit() sys.exit(1) } else if (minor.get == 8) { if (BuildInfo.javaVersion != 8) { logger.warn(s"Build and run versions disagree: build: ${BuildInfo.javaVersion}, run: ${major.get}.${minor.get}, trying anyway!") } } case None => logger.warn(s"Cannot find Java version from ${javaVersion}, continuing anyway!") } } else { if (major.get > 9) { if (major.get != BuildInfo.javaVersion) { logger.warn(s"Build and run versions disagree: build: ${BuildInfo.javaVersion}, run: ${major.get}, trying anyway!") } } else { logger.warn(s"Strange version number, please report: ${javaVersion}, continuing anyway!") } } case None => logger.warn(s"Cannot find Java version from ${javaVersion}, continuing anyway!") } logger.info(s"Running on Java Version ${javaVersion}") val userHomeDirectory = System.getProperty("user.home") val confPath = userHomeDirectory + "/.tlcockpit.conf" val props = new Properties() val propsFile = new File(confPath) if (propsFile.exists()) { props.load(new FileInputStream(propsFile)) } var tlmgrBusy = BooleanProperty(false) // necessary action when Window is closed with X or some other operation override def stopApp(): Unit = { tlmgr.cleanup() } val iconImage = new Image(getClass.getResourceAsStream("tlcockpit-48.jpg")) val logoImage = new Image(getClass.getResourceAsStream("tlcockpit-128.jpg")) val busyImage = new Image(getClass.getResourceAsStream("spinner-small.gif")) val msgFont = new Font(30f) val busySpinner: ImageView = new ImageView(busyImage) { // scaleX = 0.3 // scaleY = 0.3 } def SpinnerPlaceHolder(txt: String): Node = { val tmp = new Label(txt) tmp.wrapText = true tmp.opacity = 0.4f tmp.font = msgFont tmp.graphic = busySpinner tmp } val tlpkgs: ObservableMap[String, TLPackageShort] = ObservableMap[String,TLPackageShort]() val pkgs: ObservableMap[String, TLPackageDisplay] = ObservableMap[String, TLPackageDisplay]() val upds: ObservableMap[String, TLUpdateDisplay] = ObservableMap[String, TLUpdateDisplay]() val bkps: ObservableMap[String, Map[String, TLBackupDisplay]] = ObservableMap[String, Map[String,TLBackupDisplay]]() // pkgname -> (version -> TLBackup)* val logText: ObservableBuffer[String] = ObservableBuffer[String]() val outputText: ObservableBuffer[String] = ObservableBuffer[String]() val errorText: ObservableBuffer[String] = ObservableBuffer[String]() val outputfield: TextArea = new TextArea { editable = false wrapText = true text = "" } val logfield: TextArea = new TextArea { editable = false wrapText = true text = "" } val errorfield: TextArea = new TextArea { editable = false wrapText = true text = "" } logText.onChange({ logfield.text = logText.mkString("\n") logfield.scrollTop = Double.MaxValue }) errorText.onChange({ errorfield.text = errorText.mkString("\n") errorfield.scrollTop = Double.MaxValue if (errorfield.text.value.nonEmpty) { outerrpane.expanded = true outerrtabs.selectionModel().select(2) } }) outputText.onChange({ outputfield.text = outputText.mkString("\n") outputfield.scrollTop = Double.MaxValue }) val update_all_menu: MenuItem = new MenuItem("Update all") { val cmd: String = "--all" + { if (disable_auto_install) " --no-auto-install" else "" } + { if (disable_auto_removal) " --no-auto-remove" else "" } + { if (enable_reinstall_forcible) " --reinstall-forcibly-removed" else "" } onAction = (ae) => callback_update(cmd) disable = true } val update_self_menu: MenuItem = new MenuItem("Update self") { onAction = (ae) => callback_update("--self") disable = true } val outerrtabs: TabPane = new TabPane { minWidth = 400 tabs = Seq( new Tab { text = "Output" closable = false content = outputfield }, new Tab { text = "Logging" closable = false content = logfield }, new Tab { text = "Errors" closable = false content = errorfield } ) } val outerrpane: TitledPane = new TitledPane { text = "Debug" collapsible = true expanded = false content = outerrtabs } val cmdline = new TextField() cmdline.onKeyPressed = { (ae: KeyEvent) => if (ae.code == KeyCode.Enter) callback_run_cmdline() } // read the perl dump of ctan mirrors by converting it to JSON code and parsing it def parse_ctan_mirrors(tlroot: String): Map[String,Map[String,Seq[String]]] = { try { val fileName = tlroot + "/tlpkg/installer/ctan-mirrors.pl" val foo: String = Source.fromFile(fileName).getLines.mkString("") val jsonMirrorString = foo.substring(10).replace("=>", ":").replace("""'""", "\"").replace(";", "") val ast = jsonMirrorString.parseJson ast.convertTo[Map[String, Map[String, Map[String, Int]]]].map { contpair => (contpair._1, contpair._2.map { countrypair => (countrypair._1, countrypair._2.keys.toSeq) }) } } catch { case e: Exception => logText.append("Cannot find or parse ctan-mirrors.pl") logger.debug("Cannot find or parse ctan-mirrors.pl") Map[String,Map[String,Seq[String]]]() } } def callback_quit(): Unit = { tlmgr.cleanup() Platform.exit() sys.exit(0) } def callback_run_text(s: String): Unit = { tlmgr_send(s, (a: String, b: Array[String]) => {}) } def callback_run_cmdline(): Unit = { tlmgr_send(cmdline.text.value, (status,output) => { outputText.append(output.mkString("\n")) outerrpane.expanded = true outerrtabs.selectionModel().select(0) }) } def not_implemented_info(): Unit = { new Alert(AlertType.Warning) { initOwner(stage) title = "Warning" headerText = "This functionality is not implemented by now!" contentText = "Sorry for the inconveniences." }.showAndWait() } val OutputBuffer: ObservableBuffer[String] = ObservableBuffer[String]() var OutputBufferIndex:Int = 0 val OutputFlushLines = 100 OutputBuffer.onChange { // length is number of lines! var foo = "" OutputBuffer.synchronized( if (OutputBuffer.length - OutputBufferIndex > OutputFlushLines) { foo = OutputBuffer.slice(OutputBufferIndex, OutputBufferIndex + OutputFlushLines).mkString("") OutputBufferIndex += OutputFlushLines Platform.runLater { outputText.append(foo) } } ) } def reset_output_buffer(): Unit = { OutputBuffer.clear() OutputBufferIndex = 0 } def callback_run_external(ss: Array[String], unbuffered: Boolean = true): Unit = { outputText.clear() // logText.clear() outerrpane.expanded = true outerrtabs.selectionModel().select(0) // outputText.append(s"Running ${ss.mkString(" ")}" + (if (unbuffered) " (unbuffered)" else " (buffered)")) val foo = Future { ss.foreach { s => val runcmd = if (isCygwin) "bash -l -c \"" + s + "\"" else s Platform.runLater { outputText.append(s"Running ${s}" + (if (unbuffered) " (unbuffered)" else " (buffered)")) actionLabel.text = s"[${s}]" } runcmd ! ProcessLogger( line => if (unbuffered) Platform.runLater(outputText.append(line)) else OutputBuffer.synchronized(OutputBuffer.append(line + "\n")), line => Platform.runLater(logText.append(line)) ) } } foo.onComplete { case Success(ret) => Platform.runLater { actionLabel.text = "" outputText.append(OutputBuffer.slice(OutputBufferIndex,OutputBuffer.length).mkString("")) outputText.append("Completed") reset_output_buffer() outputfield.scrollTop = Double.MaxValue } case Failure(t) => Platform.runLater { actionLabel.text = "" outputText.append(OutputBuffer.slice(OutputBufferIndex,OutputBuffer.length).mkString("")) outputText.append("Completed") reset_output_buffer() outputfield.scrollTop = Double.MaxValue errorText.append(s"An ERROR has occurred running one of ${ss.mkString(" ")}: " + t.getMessage) errorfield.scrollTop = Double.MaxValue outerrpane.expanded = true outerrtabs.selectionModel().select(2) } } } def callback_about(): Unit = { new Alert(AlertType.Information) { initOwner(stage) title = "About TLCockpit" graphic = new ImageView(logoImage) headerText = "TLCockpit version " + version + "\n\nManage your TeX Live with speed!" contentText = "Copyright 2017-2020 Norbert Preining\nLicense: GPL3+\nSources: https://github.com/TeX-Live/tlcockpit" }.showAndWait() } // Output of update --self /* tlmgr> update --self location-url /home/norbert/public_html/tlnet /home/norbert/public_html/tlcritical /home/norbert/Domains/server/texlive.info/contrib/2017 /home/norbert/public_html/tltexjp total-bytes 381087 end-of-header texlive.infra u 4629 46295 381087 ??:?? ??:?? tlcritical - - end-of-updates STDERR running mktexlsr ... STDERR done running mktexlsr. STDERR running mtxrun --generate ... STDERR done running mtxrun --generate. OK STDOUT (with patch STDERR) tlmgr has been updated, restarting! protocol 1 tlmgr> The problem with the update function lies in the protocol 1 which is not accepted/expected by the update function! */ def set_line_update_function(mode: String): Unit = { var prevName = "" stdoutLineUpdateFunc = (l:String) => { logger.trace("DEBUG line update: " + l + "=") l match { case u if u.startsWith("location-url") => None case u if u.startsWith("total-bytes") => None case u if u.startsWith("end-of-header") => None // case u if u.startsWith("end-of-updates") => None case u if u == "OK" => None case u if u.startsWith("tlmgr>") => None case u => if (prevName != "") { if (mode == "update") { // parallelism is a pain, I get concurrent access here, but don't know with whom? // ConcurrentModificationExceptions often occur when you're modifying // a collection while you are iterating over its elements. // val newkids: ObservableBuffer[TreeItem[TLUpdateDisplay]] = // updateTable.root.value.children.filter(_.value.value.name.value != prevName) // .map(_.asInstanceOf[TreeItem[TLUpdateDisplay]]) // the last map is only necessary becasue ScalaFX is buggy and does not produce // proper types here!!! Probably similar to https://github.com/scalafx/scalafx/issues/137 // updateTable.root.value.children = newkids upds.remove(prevName) trigger_update("upds") } else if (mode == "remove") { tlpkgs(prevName).installed = false } else { // install tlpkgs(prevName).installed = true } if (mode == "remove") { pkgs(prevName).lrev = ObjectProperty[Int](0) pkgs(prevName).installed = StringProperty("Not installed") tlpkgs(prevName).lrev = 0 } else { // install and update pkgs(prevName).lrev = pkgs(prevName).rrev pkgs(prevName).installed = StringProperty("Installed") // TODO support Mixed!!! tlpkgs(prevName).lrev = tlpkgs(prevName).rrev } packageTable.refresh() } if (u.startsWith("end-of-updates")) { if (mode == "update") { Platform.runLater { updateTable.placeholder = SpinnerPlaceHolder("Post actions running") actionLabel.text = "[post actions running]" } } // nothing to be done, all has been done above logger.debug("DEBUG got end of updates") // } else if (u.startsWith("protocol ")) { // logger.debug("Got protocol line, seems tlmgr got updated and restarted!") // // nothing else to be done // } else if (u.startsWith("tlmgr has been updated, restarting")) { // logger.debug("tlmgr got updated and restarted, ignoring output") } else { logger.debug("DEBUG getting update line") prevName = if (mode == "update") { val foo = parse_one_update_line(l) val pkgname = foo.name.value upds(pkgname).status = StringProperty("Updating ...") updateTable.refresh() pkgname } else if (mode == "install") { val fields = l.split("\t") val pkgname = fields(0) pkgs(pkgname).installed = StringProperty("Installing ...") packageTable.refresh() pkgname } else { // remove val fields = l.split("\t") val pkgname = fields(0) pkgs(pkgname).installed = StringProperty("Removing ...") packageTable.refresh() pkgname } } } } } def callback_update(s: String): Unit = { val prevph = updateTable.placeholder.value set_line_update_function("update") val cmd = if (s == "--self") "update --self --no-restart" else s"update $s" tlmgr_send(cmd, (a,b) => { stdoutLineUpdateFunc = defaultStdoutLineUpdateFunc Platform.runLater { updateTable.placeholder = prevph } if (s == "--self") { reinitialize_tlmgr() // this doesn't work seemingly // update_upds_list() } }) } def callback_remove(pkg: String): Unit = { set_line_update_function("remove") tlmgr_send(s"remove $pkg", (_, _) => { stdoutLineUpdateFunc = defaultStdoutLineUpdateFunc }) } def callback_install(pkg: String): Unit = { set_line_update_function("install") tlmgr_send(s"install $pkg", (_,_) => { stdoutLineUpdateFunc = defaultStdoutLineUpdateFunc }) } def callback_restore(str: String, rev: String): Unit = { tlmgr_send(s"restore --force $str $rev", (_,_) => { tlpkgs(str).lrev = rev.toLong pkgs(str).lrev = ObjectProperty[Int](rev.toInt) packageTable.refresh() Platform.runLater { actionLabel.text = "[running post actions]" } }) } bkps.onChange( (obs,chs) => { val doit = chs match { case ObservableMap.Add(k, v) => k.toString == "root" case ObservableMap.Replace(k, va, vr) => k.toString == "root" case ObservableMap.Remove(k, v) => k.toString == "root" } if (doit) { logger.debug("DEBUG bkps.onChange called new length = " + bkps.keys.toArray.length) val newroot = new TreeItem[TLBackupDisplay](new TLBackupDisplay("root", "", "")) { children = bkps .filter(_._1 != "root") .map(p => { val pkgname: String = p._1 // we sort by negative of revision number, which give inverse sort val versmap: Array[(String, TLBackupDisplay)] = p._2.toArray.sortBy(-_._2.rev.value.toInt) val foo: Seq[TreeItem[TLBackupDisplay]] = versmap.tail.sortBy(-_._2.rev.value.toInt).map { q => new TreeItem[TLBackupDisplay](q._2) }.toSeq new TreeItem[TLBackupDisplay](versmap.head._2) { children = foo } }).toArray.sortBy(_.value.value.name.value) } Platform.runLater { backupTable.root = newroot } } }) def view_pkgs_by_collections(pkgbuf: scala.collection.mutable.Map[String, TLPackageDisplay], binbuf: scala.collection.mutable.Map[String, ArrayBuffer[TLPackageDisplay]], colbuf: scala.collection.mutable.Map[String, ArrayBuffer[TLPackageDisplay]]): Seq[TreeItem[TLPackageDisplay]] = { val bin_pkg_map = compute_bin_pkg_mapping(pkgbuf, binbuf) colbuf.map( p => { val colname: String = p._1 val coldeps: Seq[TLPackageDisplay] = p._2 val coltlpd: TLPackageDisplay = pkgbuf(colname) new TreeItem[TLPackageDisplay](coltlpd) { children = coldeps.filter(q => tlpkgs(q.name.value).category != "Collection").sortBy(_.name.value).map(sub => { val binmap: (Boolean, Seq[TLPackageDisplay]) = bin_pkg_map(sub.name.value) val ismixed: Boolean = binmap._1 val kids: Seq[TLPackageDisplay] = binmap._2.sortBy(_.name.value) val ti = if (ismixed) { // replace installed status with "Mixed" new TreeItem[TLPackageDisplay]( new TLPackageDisplay(sub.name.value, sub.lrev.value.toString, sub.rrev.value.toString, sub.shortdesc.value, sub.size.value.toString, "Mixed") ) { children = kids.map(new TreeItem[TLPackageDisplay](_)) } } else { new TreeItem[TLPackageDisplay](sub) { children = kids.map(new TreeItem[TLPackageDisplay](_)) } } ti } ) } } ).toSeq // ArrayBuffer.empty[TreeItem[TLPackageDisplay]] } def view_pkgs_by_names(pkgbuf: scala.collection.mutable.Map[String, TLPackageDisplay], binbuf: scala.collection.mutable.Map[String, ArrayBuffer[TLPackageDisplay]]): Seq[TreeItem[TLPackageDisplay]] = { val bin_pkg_map: Map[String, (Boolean, Seq[TLPackageDisplay])] = compute_bin_pkg_mapping(pkgbuf, binbuf) pkgbuf.map{ p => { val binmap: (Boolean, Seq[TLPackageDisplay]) = bin_pkg_map(p._1) val pkgtlp: TLPackageDisplay = p._2 val ismixed: Boolean = binmap._1 val kids: Seq[TLPackageDisplay] = binmap._2.sortBy(_.name.value) if (ismixed) { new TreeItem[TLPackageDisplay]( new TLPackageDisplay(pkgtlp.name.value, pkgtlp.lrev.value.toString, pkgtlp.rrev.value.toString, pkgtlp.shortdesc.value, pkgtlp.size.value.toString, "Mixed") ) { children = kids.map(new TreeItem[TLPackageDisplay](_)) } } else { new TreeItem[TLPackageDisplay](pkgtlp) { children = kids.map(new TreeItem[TLPackageDisplay](_)) } } } }.toSeq } def compute_bin_pkg_mapping(pkgbuf: scala.collection.mutable.Map[String, TLPackageDisplay], binbuf: scala.collection.mutable.Map[String, ArrayBuffer[TLPackageDisplay]]): Map[String, (Boolean, Seq[TLPackageDisplay])] = { pkgbuf.map { p => { val kids: Seq[TLPackageDisplay] = if (binbuf.keySet.contains(p._2.name.value)) { binbuf(p._2.name.value) } else { Seq() } // for ismixed we && all the installed status. If all are installed, we get true val allinstalled = (kids :+ p._2).foldRight[Boolean](true)((k, b) => k.installed.value == "Installed" && b) val someinstalled = (kids :+ p._2).exists(_.installed.value == "Installed") val mixedinstalled = !allinstalled && someinstalled (p._1, (mixedinstalled, kids)) } }.toMap } pkgs.onChange( (obs,chs) => { val doit = chs match { case ObservableMap.Add(k, v) => k.toString == "root" case ObservableMap.Replace(k, va, vr) => k.toString == "root" // don't call the trigger on root removal! // case ObservableMap.Remove(k, v) => k.toString == "root" case ObservableMap.Remove(k,v) => false } if (doit) { logger.debug("DEBUG: entering pkgs.onChange") // val pkgbuf: ArrayBuffer[TLPackageDisplay] = ArrayBuffer.empty[TLPackageDisplay] val pkgbuf = scala.collection.mutable.Map.empty[String, TLPackageDisplay] val binbuf = scala.collection.mutable.Map.empty[String, ArrayBuffer[TLPackageDisplay]] val colbuf = scala.collection.mutable.Map.empty[String, ArrayBuffer[TLPackageDisplay]] pkgs.foreach(pkg => { // complicated part, determine whether it is a sub package or not! // we strip of initial texlive. prefixes to make sure we deal // with real packages if ((pkg._1.startsWith("texlive.infra") && pkg._1.stripPrefix("texlive.infra").contains(".")) || pkg._1.stripPrefix("texlive.infra").contains(".")) { val foo: Array[String] = if (pkg._1.startsWith("texlive.infra")) Array("texlive.infra", pkg._1.stripPrefix("texlive.infra")) else pkg._1.split('.') val pkgname = foo(0) if (pkgname != "") { val binname = foo(1) if (binbuf.keySet.contains(pkgname)) { binbuf(pkgname) += pkg._2 } else { binbuf(pkgname) = ArrayBuffer[TLPackageDisplay](pkg._2) } } } else if (pkg._1 == "root") { // ignore the dummy root element, // only used for speeding up event handling } else { pkgbuf(pkg._1) = pkg._2 } }) // Another round to propagate purely .win32 packages like wintools.win32 or // dviout.win32 from binpkg status to full pkg, since they don't have // accompanying main packages binbuf.foreach(p => { if (!pkgbuf.contains(p._1)) { if (p._2.length > 1) { errorText += "THAT SHOULD NOT HAPPEN: >>" + p._1 + "<< >>" + p._2.length + "<<" p._2.foreach(f => logger.trace("-> " + f.name.value)) } else { logger.trace("DEBUG Moving " + p._2.head.name.value + " up to pkgbuf " + p._1) pkgbuf(p._2.head.name.value) = p._2.head // TODO will this work out with the foreach loop above??? binbuf -= p._1 } } }) // another loop to collection and fill the collections buffer pkgs.foreach(pkg => { if (tlpkgs.contains(pkg._1)) { if (tlpkgs(pkg._1).category == "Collection") { val foo: immutable.Seq[String] = tlpkgs(pkg._1).depends colbuf(pkg._1) = ArrayBuffer[TLPackageDisplay]() // TODO we need to deal with packages that get removed!!! // for now just make sure we don't add them here! colbuf(pkg._1) ++= foo.filter(pkgbuf.contains).map(pkgbuf(_)) } } else if (pkg._1 == "root") { // do nothing } else { errorText += "Cannot find information for " + pkg._1 } }) // now we have all normal packages in pkgbuf, and its sub-packages in binbuf // we need to create TreeItems val viewkids: Seq[TreeItem[TLPackageDisplay]] = if (ViewByPkg.selected.value) view_pkgs_by_names(pkgbuf, binbuf) else view_pkgs_by_collections(pkgbuf, binbuf, colbuf) logger.debug("DEBUG: leaving pkgs.onChange before runLater") Platform.runLater { packageTable.root = new TreeItem[TLPackageDisplay](new TLPackageDisplay("root", "0", "0", "", "0", "")) { expanded = true children = viewkids.sortBy(_.value.value.name.value) } } } }) upds.onChange( (obs, chs) => { var doit = chs match { case ObservableMap.Add(k, v) => k.toString == "root" case ObservableMap.Replace(k, va, vr) => k.toString == "root" case ObservableMap.Remove(k, v) => k.toString == "root" } if (doit) { val infraAvailable = upds.keys.exists(_.startsWith("texlive.infra")) // only allow for updates of other packages when no infra update available val updatesAvailable = !infraAvailable && upds.keys.exists(p => !p.startsWith("texlive.infra") && !(p == "root")) val newroot = new TreeItem[TLUpdateDisplay](new TLUpdateDisplay("root", "", "", "", "", "")) { children = upds .filter(_._1 != "root") .map(p => new TreeItem[TLUpdateDisplay](p._2)) .toArray .sortBy(_.value.value.name.value) } Platform.runLater { update_self_menu.disable = !infraAvailable update_all_menu.disable = !updatesAvailable updateTable.root = newroot if (infraAvailable) { texlive_infra_update_warning() } } } }) def texlive_infra_update_warning(): Unit = { new Alert(AlertType.Warning) { initOwner(stage) title = "TeX Live Infrastructure Update Available" headerText = "Updates to the TeX Live Manager (Infrastructure) available." contentText = "Please use \"Update self\" from the Menu!" }.showAndWait() } def load_backups_update_bkps_view(): Unit = { val prevph = backupTable.placeholder.value backupTable.placeholder = SpinnerPlaceHolder("Loading backups") tlmgr_send("restore --json", (status, lines) => { val jsonAst = lines.mkString("").parseJson val backups: Map[String, Map[String, TLBackupDisplay]] = jsonAst .convertTo[List[TLBackup]] .groupBy[String](_.name) .map(p => (p._1, p._2.map(q => (q.rev, new TLBackupDisplay(q.name, q.rev, q.date))).toMap)) bkps.clear() bkps ++= backups trigger_update("bkps") backupTable.placeholder = prevph }) } def update_pkgs_view(): Unit = { val newpkgs: Map[String, TLPackageDisplay] = tlpkgs .filter { p => val searchTerm = searchEntry.text.value.toLowerCase p._1.toLowerCase.contains(searchTerm) || p._2.shortdesc.getOrElse("").toLowerCase.contains(searchTerm) } .map { p => (p._2.name, new TLPackageDisplay( p._2.name, p._2.lrev.toString, p._2.rrev.toString, p._2.shortdesc.getOrElse(""), "0", if (p._2.installed) "Installed" else "Not installed" ) ) }.toMap pkgs.clear() pkgs ++= newpkgs trigger_update("pkgs") } /* def load_tlpdb_update_pkgs_view():Unit = { val prevph = packageTable.placeholder.value packageTable.placeholder = SpinnerPlaceHolder("Loading database") tlmgr_send("info --json", (status, lines) => { logger.debug(s"load tlpdb update pkgs: got status ${status}") logger.trace(s"load tlpdb update pkgs: got lines = " + lines.head) val jsonAst = lines.mkString("").parseJson tlpkgs.clear() tlpkgs ++= jsonAst.convertTo[List[TLPackage]].map { p => (p.name, p)} update_pkgs_view() packageTable.placeholder = prevph }) } */ def load_tlpdb_update_pkgs_view_no_json():Unit = { val prevph = packageTable.placeholder.value packageTable.placeholder = SpinnerPlaceHolder("Loading database") tlmgr_send("info --data name,localrev,remoterev,category,size,installed,depends,shortdesc", (status, lines) => { logger.debug(s"load tlpdb update (no json) pkgs: got status ${status}") logger.trace(s"load tlpdb update (no json) pkgs: got lines = " + lines.head) val newtlpkgs: Map[String, TLPackageShort] = lines.map(l => { // logger.debug(s"got line >>>${l}<<<") val fields = l.split(",",8) val pkgname = fields(0) val shortdesc = fields(7).stripPrefix(""""""").stripSuffix(""""""").replaceAll("""\\"""",""""""") val lrev = fields(1).toLong val rrev = fields(2).toLong val cat = fields(3) val size = fields(4).toLong val installed = fields(5) == "1" val depends = fields(6).split(":").toList TLPackageShort(pkgname, if (shortdesc == "") None else Some(shortdesc), lrev, rrev, cat, depends, installed, lrev > 0) }).map{ p => logger.trace("Constructed TLPackage: " + p) (p.name, p) }.toMap tlpkgs.clear() tlpkgs ++= newtlpkgs update_pkgs_view() packageTable.placeholder = prevph }) } def parse_one_update_line(l: String): TLUpdateDisplay = { logger.debug(s"Got update line >>${l}") val fields = l.split("\t") logger.debug(s"Splitting into ${fields}") val pkgname = fields(0) val status = fields(1) match { case "d" => "Removed on server" case "f" => "Forcibly removed" case "u" => "Update available" case "r" => "Local is newer" case "a" => "New on server" case "i" => "Not installed" case "I" => "Reinstall" } val localrev = fields(2) val serverrev = fields(3) val size = if (fields(1) == "d") "0" else humanReadableByteSize(fields(4).toLong) val runtime = fields(5) val esttot = fields(6) val tag = fields(7) val lctanv = fields(8) val rctanv = fields(9) val tlpkg: TLPackageDisplay = pkgs(pkgname) val shortdesc = tlpkg.shortdesc.value new TLUpdateDisplay(pkgname, status, localrev + { if (lctanv != "-") s" ($lctanv)" else "" }, serverrev + { if (rctanv != "-") s" ($rctanv)" else "" }, shortdesc, size) } def load_updates_update_upds_view(): Unit = { val prevph = updateTable.placeholder.value updateTable.placeholder = SpinnerPlaceHolder("Loading updates") tlmgr_send("update --list", (status, lines) => { logger.debug(s"got updates length ${lines.length}") logger.trace(s"tlmgr last output = ${lines}") val newupds: Map[String, TLUpdateDisplay] = lines.filter { case u if u.startsWith("location-url") => false case u if u.startsWith("total-bytes") => false case u if u.startsWith("end-of-header") => false case u if u.startsWith("end-of-updates") => false case u => true }.map { l => val foo = parse_one_update_line(l) (foo.name.value, foo) }.toMap val infraAvailable = newupds.keys.exists(_.startsWith("texlive.infra")) upds.clear() if (infraAvailable) { upds ++= Seq( ("texlive.infra", newupds("texlive.infra") ) ) } else { upds ++= newupds } trigger_update("upds") updateTable.placeholder = prevph }) } def trigger_update(s:String): Unit = { logger.debug("DEBUG: Triggering update of " + s) if (s == "pkgs") { pkgs("root") = new TLPackageDisplay("root", "0", "0", "", "0", "") } else if (s == "upds") { upds("root") = new TLUpdateDisplay("root", "", "", "", "", "") } else if (s == "bkps") { bkps("root") = Map[String, TLBackupDisplay](("0", new TLBackupDisplay("root", "0", "0"))) } } def doListView(files: Seq[String], clickable: Boolean): scalafx.scene.Node = { if (files.length <= 5) { val vb = new VBox() vb.children = files.map { f => val fields = f.split(" ") new Label(fields(0)) { if (clickable) { textFill = Color.Blue onMouseClicked = { me: MouseEvent => OsTools.openFile(tlmgr.tlroot + "/" + fields(0)) } cursor = Cursor.Hand } } } vb } else { val vb = new ListView[String] {} vb.minHeight = 150 vb.prefHeight = 150 vb.maxHeight = 200 vb.vgrow = Priority.Always vb.orientation = Orientation.Vertical vb.cellFactory = { p => { val foo = new ListCell[String] foo.item.onChange { (_, _, str) => foo.text = str } if (clickable) { foo.textFill = Color.Blue foo.onMouseClicked = { me: MouseEvent => OsTools.openFile(tlmgr.tlroot + "/" + foo.text.value) } foo.cursor = Cursor.Hand } foo } } // vb.children = docFiles.map { f => vb.items = ObservableBuffer(files.map { f => val fields = f.split(" ") fields(0) }) vb } } val mainMenu: Menu = new Menu("TLCockpit") { items = List( // temporarily move here as we disable the Help menu new MenuItem("About") { onAction = (ae) => callback_about() }, new MenuItem("Exit") { onAction = (ae: ActionEvent) => callback_quit() }) } val toolsMenu: Menu = new Menu("Tools") { items = List( new MenuItem("Update filename databases ...") { onAction = (ae) => { callback_run_external(Array("mktexlsr", "mtxrun --generate")) // callback_run_external("mtxrun --generate") } }, // too many lines are quickly output -> GUI becomes hanging until // all the callbacks are done - call fmtutil with unbuffered = false new MenuItem("Rebuild all formats ...") { onAction = (ae) => callback_run_external(Array("fmtutil --sys --all"), false) }, new MenuItem("Update font map database ...") { onAction = (ae) => callback_run_external(Array("updmap --sys")) } ) } val ViewByPkg = new RadioMenuItem("by package name") { onAction = (ae) => { searchEntry.text = "" update_pkgs_view() } } val ViewByCol = new RadioMenuItem("by collections") { onAction = (ae) => { searchEntry.text = "" update_pkgs_view() } } ViewByPkg.selected = true ViewByCol.selected = false val pkgsMenu: Menu = new Menu("Packages") { val foo = new ToggleGroup foo.toggles = Seq(ViewByPkg, ViewByCol) items = List(ViewByPkg, ViewByCol) } var disable_auto_removal = false var disable_auto_install = false var enable_reinstall_forcible = false val updMenu: Menu = new Menu("Updates") { items = List( update_all_menu, update_self_menu, new SeparatorMenuItem, new CheckMenuItem("Disable auto removal") { onAction = (ae) => disable_auto_removal = selected.value }, new CheckMenuItem("Disable auto install") { onAction = (ae) => disable_auto_install = selected.value }, new CheckMenuItem("Reinstall forcibly removed") { onAction = (ae) => enable_reinstall_forcible = selected.value } ) } def callback_general_options(): Unit = { tlmgr_send("option showall --json", (status, lines) => { val jsonAst = lines.mkString("").parseJson val tlpdopts: List[TLOption] = jsonAst.convertTo[List[TLOption]] Platform.runLater { val dg = new OptionsDialog(tlpdopts) dg.showAndWait() match { case Some(changedOpts) => changedOpts.foreach(p => { // don't believe it or not, but \" does *NOT* work in Scala in // interpolated strings, and it seems there is no better way // than that one ... tlmgr_send(s"option ${p._1} ${'"'}${p._2}${'"'}", (_,_) => None) }) case None => } } }) } def callback_paper(): Unit = { tlmgr_send("paper --json", (status, lines) => { val jsonAst = lines.mkString("").parseJson val paperconfs: Map[String, TLPaperConf] = jsonAst.convertTo[List[TLPaperConf]].map { p => (p.program, p) }.toMap val currentPapers: Map[String, String] = paperconfs.mapValues(p => p.options.head) Platform.runLater { val dg = new PaperDialog(paperconfs) dg.showAndWait() match { case Some(newPapers) => logger.debug(s"Got result ${newPapers}") // collect changed settings val changedPapers = newPapers.filter(p => currentPapers(p._1) != p._2) logger.debug(s"Got changed papers ${changedPapers}") changedPapers.foreach(p => { tlmgr_send(s"paper ${p._1} paper ${p._2}", (_,_) => None) }) case None => } } }) } def callback_pkg_info(pkg: String) = { tlmgr_send(s"info --json ${pkg}", (status, lines) => { try { val jsonAst = lines.mkString("").parseJson val tlpkgs = jsonAst.convertTo[List[TLPackage]] Platform.runLater { new PkgInfoDialog(tlpkgs.head).showAndWait() } } catch { case foo : spray.json.DeserializationException => new Alert(AlertType.Warning) { initOwner(stage) title = "Warning" headerText = s"Cannot display information for ${pkg}" contentText = s"We couldn't parse the output of\ntlmgr info --json ${pkg}\n" }.showAndWait() case bar : ArrayIndexOutOfBoundsException => new Alert(AlertType.Warning) { initOwner(stage) title = "Warning" headerText = s"Cannot find information for ${pkg}" contentText = s"We couldn't find information for ${pkg}\n" }.showAndWait() } }) } def save_properties(): Unit = { props.store(new FileOutputStream(confPath), null) } val StartTabPkgs = new RadioMenuItem("Packages") { onAction = (ae) => { props.setProperty("StartupTab", "packages") save_properties() } } val StartTabUpds = new RadioMenuItem("Updates") { onAction = (ae) => { props.setProperty("StartupTab","updates") save_properties() } } val StartTabBcks = new RadioMenuItem("Backups") { onAction = (ae) => { props.setProperty("StartupTab","backups") save_properties() } } val startupTabMenu: Menu = new Menu("Startup Tab") { val foo = new ToggleGroup foo.toggles = Seq(StartTabPkgs, StartTabUpds, StartTabBcks) items = List(StartTabPkgs, StartTabUpds, StartTabBcks) } val optionsMenu: Menu = new Menu("Options") { items = List( new MenuItem("General ...") { onAction = (ae) => callback_general_options() }, new MenuItem("Paper ...") { onAction = (ae) => callback_paper() }, startupTabMenu /* new MenuItem("Platforms ...") { disable = true; onAction = (ae) => not_implemented_info() }, new SeparatorMenuItem, new CheckMenuItem("Expert options") { disable = true }, new CheckMenuItem("Enable debugging options") { disable = true }, new CheckMenuItem("Disable auto-install of new packages") { disable = true }, new CheckMenuItem("Disable auto-removal of server-deleted packages") { disable = true } */ ) } val expertPane: TitledPane = new TitledPane { text = "Experts only" collapsible = true expanded = false content = new VBox { spacing = 10 children = List( new HBox { spacing = 10 alignment = Pos.CenterLeft children = List( new Label("tlmgr shell command:"), cmdline, new Button { text = "Go" onAction = (event: ActionEvent) => callback_run_cmdline() } ) } ) } } val updateTable: TreeTableView[TLUpdateDisplay] = { val colName = new TreeTableColumn[TLUpdateDisplay, String] { text = "Package" cellValueFactory = { _.value.value.value.name } prefWidth = 150 } val colStatus = new TreeTableColumn[TLUpdateDisplay, String] { text = "Status" cellValueFactory = { _.value.value.value.status } prefWidth = 120 } val colDesc = new TreeTableColumn[TLUpdateDisplay, String] { text = "Description" cellValueFactory = { _.value.value.value.shortdesc } prefWidth = 300 } val colLRev = new TreeTableColumn[TLUpdateDisplay, String] { text = "Local rev" cellValueFactory = { _.value.value.value.lrev } prefWidth = 100 } val colRRev = new TreeTableColumn[TLUpdateDisplay, String] { text = "Remote rev" cellValueFactory = { _.value.value.value.rrev } prefWidth = 100 } val colSize = new TreeTableColumn[TLUpdateDisplay, String] { text = "Size" cellValueFactory = { _.value.value.value.size } prefWidth = 70 } val table = new TreeTableView[TLUpdateDisplay]( new TreeItem[TLUpdateDisplay](new TLUpdateDisplay("root","","","","","")) { expanded = false }) { columns ++= List(colName, colStatus, colDesc, colLRev, colRRev, colSize) } colDesc.prefWidth.bind(table.width - colName.width - colLRev.width - colRRev.width - colSize.width - colStatus. width - 15) table.prefHeight = 300 table.vgrow = Priority.Always table.placeholder = new Label("No updates available") { opacity = 0.4f font = msgFont } table.showRoot = false table.rowFactory = { _ => val row = new TreeTableRow[TLUpdateDisplay] {} val infoMI = new MenuItem("Info") { onAction = (ae) => callback_pkg_info(row.item.value.name.value) } val updateMI = new MenuItem("Update") { onAction = (ae) => callback_update(row.item.value.name.value) } val installMI = new MenuItem("Install") { onAction = (ae) => callback_install(row.item.value.name.value) } val removeMI = new MenuItem("Remove") { onAction = (ae) => callback_remove(row.item.value.name.value) } val ctm = new ContextMenu(infoMI, updateMI, installMI, removeMI) row.item.onChange { (_,_,newTL) => if (newTL != null) { if (newTL.status.value == "New on server") { installMI.disable = false removeMI.disable = true updateMI.disable = true } else if (newTL.status.value == "Removed on server") { installMI.disable = true removeMI.disable = false updateMI.disable = true } else { installMI.disable = true removeMI.disable = false updateMI.disable = false } } } row.contextMenu = ctm row } table } val packageTable: TreeTableView[TLPackageDisplay] = { val colName = new TreeTableColumn[TLPackageDisplay, String] { text = "Package" cellValueFactory = { _.value.value.value.name } prefWidth = 150 } val colDesc = new TreeTableColumn[TLPackageDisplay, String] { text = "Description" cellValueFactory = { _.value.value.value.shortdesc } prefWidth = 300 } val colInst = new TreeTableColumn[TLPackageDisplay, String] { text = "Installed" cellValueFactory = { _.value.value.value.installed } prefWidth = 100 } val table = new TreeTableView[TLPackageDisplay]( new TreeItem[TLPackageDisplay](new TLPackageDisplay("root","0","0","","0","")) { expanded = false }) { columns ++= List(colName, colDesc, colInst) } colDesc.prefWidth.bind(table.width - colInst.width - colName.width - 15) table.prefHeight = 300 table.showRoot = false table.vgrow = Priority.Always table.rowFactory = { p => val row = new TreeTableRow[TLPackageDisplay] {} val infoMI = new MenuItem("Info") { onAction = (ae) => callback_pkg_info(row.item.value.name.value) } val installMI = new MenuItem("Install") { onAction = (ae) => callback_install(row.item.value.name.value) } val removeMI = new MenuItem("Remove") { onAction = (ae) => callback_remove(row.item.value.name.value) } val ctm = new ContextMenu(infoMI, installMI, removeMI) row.item.onChange { (_,_,newTL) => if (newTL != null) { val is_installed: Boolean = !(newTL.installed.value == "Not installed") installMI.disable = is_installed removeMI.disable = !is_installed } } row.contextMenu = ctm row } table } val backupTable: TreeTableView[TLBackupDisplay] = { val colName = new TreeTableColumn[TLBackupDisplay, String] { text = "Package" cellValueFactory = { _.value.value.value.name } prefWidth = 150 } val colRev = new TreeTableColumn[TLBackupDisplay, String] { text = "Revision" cellValueFactory = { _.value.value.value.rev } prefWidth = 100 } val colDate = new TreeTableColumn[TLBackupDisplay, String] { text = "Date" cellValueFactory = { _.value.value.value.date } prefWidth = 300 } val table = new TreeTableView[TLBackupDisplay]( new TreeItem[TLBackupDisplay](new TLBackupDisplay("root","","")) { expanded = false }) { columns ++= List(colName, colRev, colDate) } colDate.prefWidth.bind(table.width - colRev.width - colName.width - 15) table.prefHeight = 300 table.showRoot = false table.vgrow = Priority.Always table.placeholder = new Label("No backups available") { opacity = 0.4f font = msgFont } table.rowFactory = { _ => val row = new TreeTableRow[TLBackupDisplay] {} val ctm = new ContextMenu( new MenuItem("Info") { onAction = (ae) => callback_pkg_info(row.item.value.name.value) }, new MenuItem("Restore") { onAction = (ae) => callback_restore(row.item.value.name.value, row.item.value.rev.value) } ) row.contextMenu = ctm row } table } val searchEntry = new TextField() searchEntry.hgrow = Priority.Sometimes searchEntry.onKeyPressed = { (ae: KeyEvent) => if (ae.code == KeyCode.Enter) update_pkgs_view() } val statusLabel = new Label("Idle") val actionLabel = new Label("") { hgrow = Priority.Always maxWidth = Double.MaxValue } val statusBox = new HBox { children = Seq( new Label("Tlmgr status:") { vgrow = Priority.Always alignmentInParent = Pos.CenterLeft }, statusLabel, actionLabel ) maxWidth = Double.MaxValue hgrow = Priority.Always alignment = Pos.Center alignmentInParent = Pos.CenterLeft padding = Insets(10) spacing = 10 } val searchBox = new HBox { children = Seq( new Label("Search:") { vgrow = Priority.Always alignmentInParent = Pos.CenterLeft }, searchEntry, new Button("Go") { onAction = _ => update_pkgs_view() }, new Button("Reset") { onAction = _ => { searchEntry.text = "" update_pkgs_view() } } ) alignment = Pos.Center // do not add padding at the bottom as we have one from the status field padding = Insets(10,10,0,10) spacing = 10 } val pkgsContainer = new VBox { children = Seq(packageTable,searchBox) } val pkgstabs: TabPane = new TabPane { minWidth = 400 vgrow = Priority.Always tabs = Seq( new Tab { text = "Packages" closable = false content = pkgsContainer }, new Tab { text = "Updates" closable = false content = updateTable }, new Tab { text = "Backups" closable = false content = backupTable } ) } // val spacerMenu: Menu = new Menu(" ") // spacerMenu.disable = true // spacerMenu.hgrow = Priority.Always val menuBar: MenuBar = new MenuBar { useSystemMenuBar = true // menus.addAll(mainMenu, optionsMenu, helpMenu) menus.addAll(mainMenu, pkgsMenu, toolsMenu, optionsMenu) } var updLoaded = false var bckLoaded = false pkgstabs.selectionModel().selectedItem.onChange( (a,b,c) => { if (a.value.text() == "Backups") { if (!bckLoaded) { load_backups_update_bkps_view() bckLoaded = true } menuBar.menus = Seq(mainMenu, toolsMenu, optionsMenu) } else if (a.value.text() == "Updates") { // only update if not done already if (!updLoaded) { load_updates_update_upds_view() updLoaded = true } menuBar.menus = Seq(mainMenu, updMenu, toolsMenu, optionsMenu) } else if (a.value.text() == "Packages") { menuBar.menus = Seq(mainMenu, pkgsMenu, toolsMenu, optionsMenu) } } ) stage = new PrimaryStage { title = "TLCockpit" scene = new Scene { root = { // val topBox = new HBox { // children = List(menuBar, statusLabel) // } // topBox.hgrow = Priority.Always // topBox.maxWidth = Double.MaxValue val topBox = menuBar val centerBox = new VBox { padding = Insets(10) children = List(pkgstabs, statusBox, expertPane, outerrpane) } new BorderPane { // padding = Insets(20) top = topBox // left = leftBox center = centerBox // bottom = bottomBox } } } icons.add(iconImage) } stage.onCloseRequest = (e: Event) => callback_quit() stage.width = 800 val selectedTab = props.getOrDefault("StartupTab", "packages") StartTabPkgs.selected = true StartTabUpds.selected = false StartTabBcks.selected = false var StartupTab = 0 selectedTab match { case "packages" => {} case "updates" => { StartTabPkgs.selected = false StartTabUpds.selected = true StartupTab = 1 } case "backups" => { StartTabPkgs.selected = false StartTabBcks.selected = true StartupTab = 2 } case _ => { logger.warn(s"Unrecognized setting for StartupTab in config file: $selectedTab") } } var currentPromise = Promise[(String,Array[String])]() val pendingJobs = scala.collection.mutable.Queue[(String,(String, Array[String]) => Unit)]() def initialize_tlmgr(): TlmgrProcess = { tlmgrBusy.value = true actionLabel.text = "[initializing tlmgr]" // create new sync vars for each process val outputLine = new SyncVar[String] val errorLine = new SyncVar[String] val tt = new TlmgrProcess( (s: String) => { logger.trace(s"outputline put ${s}") outputLine.put(s) }, (s: String) => { logger.trace(s"errorline put ${s}") errorLine.put(s) } ) if (!tt.start_process()) { logger.debug("Cannot start tlmgr process, terminating!") Platform.exit() sys.exit(1) } logger.debug("initialize_tlmgr: sleeping after starting process") Thread.sleep(1000) /* val tlmgrMonitor = Future { while (true) { if (!tlmgr.isAlive) { logger.debug("TLMGR HAS DIED!!!!") // Platform.exit() // sys.exit(1) } Thread.sleep(5000) } }*/ val stdoutFuture = Future { val tlmgrOutput = ArrayBuffer[String]() var tlmgrStatus = "" var alive = true logger.debug("initialize tlmgr: starting stdout reader thread") while (alive) { logger.trace("stdout reader before outputLine.take") val s = outputLine.take logger.trace("stdout reader after outputLine.take") if (s == null) { alive = false logger.debug("got null from stdout, tlmgr seems to be terminated for restart") if (!currentPromise.isCompleted) { logger.debug(s"Fulfilling remaining open promise with ${tlmgrStatus} and ${tlmgrOutput.toArray.mkString}") currentPromise.success((tlmgrStatus, tlmgrOutput.toArray)) } tlmgrStatus = "" tlmgrOutput.clear() tlmgrBusy.value = false } else { logger.trace(s"DEBUG: got ==" + s + "==") if (s == "OK") { tlmgrStatus = s } else if (s == "ERROR") { tlmgrStatus = s } else if (s == "tlmgr> ") { logger.trace(s"Fulfilling current promise with ${tlmgrStatus} and ${tlmgrOutput.toArray.mkString}!") if (!currentPromise.isCompleted) { currentPromise.success((tlmgrStatus, tlmgrOutput.toArray)) } tlmgrStatus = "" tlmgrOutput.clear() tlmgrBusy.value = false if (pendingJobs.nonEmpty) { logger.debug("pending Job found!") val nextCmd = pendingJobs.dequeue() logger.debug(s"running ${nextCmd._1}") tlmgr_run_one_cmd(nextCmd._1, nextCmd._2) } } else { tlmgrOutput += s stdoutLineUpdateFunc(s) } } } logger.debug("initialize tlmgr: finishing stdout reader thread") } tlmgrBusy.onChange({ Platform.runLater { statusLabel.text = if (tlmgrBusy.value) "Busy" else "Idle" if (!tlmgrBusy.value) actionLabel.text = "" } }) stdoutFuture.onComplete { case Success(value) => logger.debug(s"tlmgr stdout reader terminated: ${value}") case Failure(e) => logger.debug(s"tlmgr stdout reader terminated with error: ${e}") } val stderrFuture = Future { var alive = true logger.debug("initialize tlmgr: starting stderr reader thread") while (alive) { val s = errorLine.take if (s == null) alive = false else stderrLineUpdateFunc(s) } logger.debug("initialize tlmgr: finishing stderr reader thread") } stderrFuture.onComplete { case Success(value) => logger.debug(s"tlmgr stderr reader terminated: ${value}") case Failure(e) => logger.debug(s"tlmgr stderr reader terminated with failure: ${e}") } tt } def tlmgr_post_init() = { // check for tlmgr revision tlmgr_send("version", (status,output) => { logger.debug(s"Callback after version, got ${status} and ${output.mkString("\n")}") output.foreach ( l => { if (l.startsWith("revision ")) { val tlmgrRev = l.stripPrefix("revision ") if (tlmgrRev == "unknown") { logger.debug("Unknown tlmgr revision, assuming git/svn version") logText += "Unknown tlmgr revision, assuming git/svn version" } else { if (tlmgrRev.toInt < 45838) { new Alert(AlertType.Error) { initOwner(stage) title = "TeX Live Manager tlmgr is too old" headerText = "TeX Live Manager tlmgr is too old" contentText = "Please update from the command line\nusing 'tlmgr update --self'\nTerminating!" }.showAndWait() callback_quit() } } } }) pkgs.clear() upds.clear() bkps.clear() logger.debug("Before loading tlpdb") load_tlpdb_update_pkgs_view_no_json() logger.debug("after loading tlpdb") pkgstabs.selectionModel().select(StartupTab) }) } def tlmgr_run_one_cmd(s: String, onCompleteFunc: (String, Array[String]) => Unit): Unit = { currentPromise = Promise[(String, Array[String])]() tlmgrBusy.value = true Platform.runLater { actionLabel.text = s"[${s}]" } currentPromise.future.onComplete { case Success((a, b)) => logger.debug("tlmgr run one cmd: current future completed!") Platform.runLater { logger.debug("tlmgr run one cmd: running on complete function") onCompleteFunc(a, b) } case Failure(ex) => logger.debug("Running tlmgr command did no succeed" + ex.getMessage) errorText += "Running a tlmgr command did not succeed: " + ex.getMessage } logger.debug(s"sending to tlmgr: ${s}") tlmgr.send_command(s) } def tlmgr_send(s: String, onCompleteFunc: (String, Array[String]) => Unit): Unit = { // logText.clear() outputText.clear() // don't close debug panel when it is open // outerrpane.expanded = false if (!currentPromise.isCompleted) { logger.debug(s"tlmgr busy, put onto pending jobs: $s") logger.debug("Currently running job: " + currentPromise) pendingJobs += ((s, onCompleteFunc)) } else { logger.debug(s"tlmgr_send sending ${s}") tlmgr_run_one_cmd(s, onCompleteFunc) } } def reinitialize_tlmgr(): Unit = { logger.debug("reinit tlmgr: entering, clearing pending jobs") pendingJobs.clear() logger.debug("reinit tlmgr: cleared pending jobs") // if (!currentPromise.isCompleted) { // logger.debug("reinit tlmgr: current promise not complete, completing it") // currentPromise.success(("",Array[String]())) // logger.debug("reinit tlmgr: after completing current promise") // } logger.debug("reinit tlmgr: cleaning up tlmgr") tlmgr.cleanup() logger.debug("reinit tlmgr: sleeping 1s") Thread.sleep(1000) logger.debug("reinit tlmgr: initializaing new tlmgr") tlmgr = initialize_tlmgr() logger.debug("reinit tlmgr: finished") tlmgr_post_init() logger.debug("reinit tlmgr: post init done") updLoaded = false pkgstabs.getSelectionModel.select(0) } def defaultStdoutLineUpdateFunc(l: String) : Unit = { logger.trace(s"DEBUG: got ==$l== from tlmgr") } def defaultStderrLineUpdateFunc(l: String) : Unit = { Platform.runLater { logText.append(l) } } var stdoutLineUpdateFunc: String => Unit = defaultStdoutLineUpdateFunc var stderrLineUpdateFunc: String => Unit = defaultStderrLineUpdateFunc var tlmgr = initialize_tlmgr() tlmgr_post_init() } // object ApplicationMain // vim:set tabstop=2 expandtab : //