/* Copyright 2010 Aaron J. Radke Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // vim: set ts=3 sw=3 et: package cc.drx import scala.sys.process.Process import java.io.OutputStream object OS{ //helper function for OS kinss for version maps (keeps the compile tree small) private def versionMap(str:String):Map[Version,String] = str.split(',').map{d => val List(k,v) = d.split('=').toList Version(k) -> v }.toMap //--os types (Kind) sealed trait Kind { //--required def name:String //--overidable def codenames:Map[Version,String] = Map.empty[Version,String] //--overidable def codename:String = codenames.get(OS.version take 2) getOrElse "" final override def toString:String = if(codename == "") name else name + codename } //--Windows (Kind) case object Windows extends Kind { val name = "Windows" //https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions override lazy val codenames = versionMap("10.0=10,6.3=8.1,6.2=8,6.1=7,6.0=Vista,5.2=XP Pro,5.1=XP,4.9=ME,5.0=2000,4.10=98") } //--Unix (Kind) sealed trait Unix extends Kind case object MacOS extends Unix{ val name = "MacOS" // https://en.wikipedia.org/wiki/MacOS override lazy val codenames = versionMap("10.7=Lion,10.8=Mountain Lion,10.9=Mavericks,10.10=Yosemite,10.11=El Capitan,10.12=Sierra,10.13=High Sierra,10.14=Mojave,10.15=Catalina,11.0=Big Sur") } case object Linux extends Unix{ val name = "MacOS" } //--Unknown (Kind) case object Unknown extends Kind{ val name = "Unknown" } //--property getters def prop(key:String):String = System.getProperty(key) //FIXME thee are dangerous null producers def env(key:String):String = System.getenv(key) lazy val Env = StringMap(scala.sys.env) lazy val Prop = StringMap(scala.sys.props.toMap) //make the bidirectional mutable map immutable at the point of eval lazy val Conf = StringMap(scala.sys.env ++ scala.sys.props.toMap) //--runtime java and scala versions TODO shouldn't this be under the version constructor? lazy val javaVersion:Version = Version(prop("java.version")) lazy val java7:Boolean = javaVersion.similar(Version("1.7.x"),2) lazy val java8:Boolean = javaVersion.similar(Version("1.8.x"),2) lazy val java9:Boolean = javaVersion.similar(Version("1.9.x"),2) lazy val java10:Boolean = javaVersion.similar(Version("10.x"),1) lazy val java11:Boolean = javaVersion.similar(Version("11.x"),1) lazy val java12:Boolean = javaVersion.similar(Version("12.x"),1) lazy val java13:Boolean = javaVersion.similar(Version("13.x"),1) lazy val java14:Boolean = javaVersion.similar(Version("14.x"),1) lazy val java15:Boolean = javaVersion.similar(Version("15.x"),1) lazy val java16:Boolean = javaVersion.similar(Version("16.x"),1) lazy val java17:Boolean = javaVersion.similar(Version("17.x"),1) lazy val scalaVersion:Version = Version(scala.util.Properties.versionString.dropWhile(!_.isDigit)) lazy val scala213:Boolean = scalaVersion.similar(Version("2.13.x"), 2) lazy val scala212:Boolean = scalaVersion.similar(Version("2.12.x"), 2) lazy val scala211:Boolean = scalaVersion.similar(Version("2.11.x"), 2) lazy val scala210:Boolean = scalaVersion.similar(Version("2.10.x"), 2) lazy val scala3:Boolean = scalaVersion.similar(Version("3.x.x"), 1) //TODO lazy val graalNative:Boolean = ???//vm == "Graal??" //java.vm.name lazy val graalJVM:Boolean = vm startsWith "GraalVM" //java.vm.name lazy val scalaJVM:Boolean = vm startsWith "Java HotSpot" //java.vm.name lazy val scalaJS:Boolean = vm startsWith "Scala.js" //java.vm.name. lazy val scalaNative:Boolean = vm startsWith "Scala Native" //java.vm.name // lazy val drx:Version = ??? //--special properties lazy val name = prop("os.name") lazy val vm = prop("java.vm.name") lazy val arch = prop("os.arch") // lazy val bit:Int = prop/"os.arch.data.model" | 64 //default 64 FIXME lookup arch name to get actual bits lazy val version = Version(prop("os.version")) lazy val user = prop("user.name") lazy val pwd:File = Prop / "user.dir" | File(".").canon lazy val home = File(prop("user.home")) lazy val kind:Kind = { val n = name.toLowerCase if(n contains "mac") MacOS else if(n contains "win") Windows else if(n contains "linux") Linux else Unknown } lazy val isMac = kind == MacOS lazy val isWin = kind == Windows lazy val isLinux = kind == Linux //--fantastic robust detection trick from https://stackoverflow.com/a/22039095/622016 private lazy val traceClasses = new Exception().getStackTrace.map{_.getClassName} lazy val isSbt = traceClasses contains "sbt.Run" lazy val isBt = traceClasses contains "xsbt.boot.Boot" lazy val isLauncher= isSbt || isBt private def pathList(path:String):List[File] = ( path.trim split prop("path.separator") map {_.trim} filter (_ != "") map File.apply map {_.canon} ).toList lazy val path:List[File] = pathList( prop("java.library.path") ) lazy val classpath:List[File] = pathList( prop("java.class.path") ) lazy val bootpath:List[File] = pathList( prop("sun.boot.library.path") ) lazy val bootClasspath:List[File] = pathList( prop("sun.boot.class.path") ) def where(basename:String) = for(p <- path; f <- p.list; if f.isFile && f.basename == basename) yield f private lazy val runtime = java.lang.Runtime.getRuntime lazy val cpus = runtime.availableProcessors lazy val bit = System.getProperty("sun.arch.data.model").toInt case class Memory(free:Bytes,total:Bytes,max:Bytes){ lazy val used:Bytes = total - free } def heap = Memory(Bytes(runtime.freeMemory), Bytes(runtime.totalMemory), Bytes(runtime.maxMemory)) def envOption(k:String):Option[String] = Option(env(k)) def computer:String = envOption("COMPUTERNAME") orElse envOption("HOSTNAME") getOrElse "localhost" private lazy val localHost = java.net.InetAddress.getLocalHost lazy val hostname:String = localHost.getHostName lazy val ip:String = localHost.getHostAddress // lazy val info:String = Macro.ksonColor(kind.name,arch,version,javaVersion,scalaVersion,heap.max,cpus,bit,user) lazy val info:String = List("name"->kind.name, "arch"->arch, "version"->version, "java"->javaVersion, "scala"->scalaVersion, "heap"->heap.max, "cpus"->cpus, "bit"->bit, "user"->user, "codename" -> kind.codename, "computer" -> computer) .map{case (k,v) => k.toString -> v.toString}.map(Format.kvColor).mkString(" ") def drive = File.roots mapWith {_.driveSize} private def wmic(domain:String, keys:Seq[String]):Map[String,String] = { import Implicit.ec lazy val KeyValue = """(\w+)=(.+)""".r Shell("wmic",domain,"get",keys mkString ",", "/format:list").lines.blockWithoutWarning(1.s).getOrElse(Nil).collect{ case KeyValue(k,v) => k -> v }.toMap } def ram:Option[Memory] = kind match { case Windows => val keys = Vector("FreePhysicalMemory","TotalVisibleMemorySize","TotalVirtualMemorySize") val kvs = wmic("os", keys).map{case (k,v) => k -> Bytes(v.toInt)}.toMap for(free <- kvs get keys(0); total <- kvs get keys(1); max <- kvs get keys(2)) yield Memory(free, total, max) case _:Unix => None //TODO add an implementation case _ => None } //--launchers private lazy val desktop = java.awt.Desktop.getDesktop def open(f:File) = desktop.open(f.canon.file) def edit(f:File) = desktop.edit(f.canon.file) def browse(f:File) = desktop.browse(f.canon.file.toURI) def browse(uri:String) = desktop.browse(new java.net.URI(uri)) def open(uri:String) = browse(uri) //--process kill //this Pid class acts as the common interface between linux and windows for 'ps' & 'kill' case class Pid(pid:Int, cmd:String){ def kill = OS.kill(pid) } //TODO replace the process calls with the Shell object type at all?? /**this (blocking) run is a simple way to use the cc.drx.Shell utility but but make several simple choices * 1.) blocks for 1.min ??? this is bad * 2.) unwraps the future and the try (returns an empty list in failure cases * 3.) automatically chooses an global executionContext * 4.) easy api for adding working directory locatiou */ // def run(cmd:String, cwd:File):List[String] = Shell(cmd,Some(cwd),List.empty).run(Implicit.ec).block(1.minute).getOrElse(Nil) // def run(cmd:String):List[String] = Shell(cmd).run(Implicit.ec).block(1.minute).getOrElse(Nil) // def run(cmd:String):Future[List[String]] = Shell(cmd).lines private lazy val WMICPid = """[^,]*,([^,]*),(\d+)""".r private lazy val PSPid = """\s*\d+\s+(\d+)\s+(.*)""".r def ps:Iterator[Pid] = kind match { case Windows => for(WMICPid(cmd,pid) <- Process("""wmic process get commandline,processid /format:csv""").lineIterator) yield Pid(pid.toInt,cmd) case _:Unix => for(PSPid(pid,cmd) <- Process(s"ps -u $user").lineIterator) yield Pid(pid.toInt,cmd) case _ => ??? } //convenience functions to filter processes searching for a key within a process def ps(key:String):Vector[Pid] = (for(p <- ps if p.cmd contains key ) yield p).toVector //convinence functions to provide a jps like solution for systems without jps installed def jps:Vector[Pid] = (for(p <- ps if p.cmd.toLowerCase contains "java") yield p).toVector def jps(key:String):Vector[Pid] = (for(p <- jps if p.cmd contains key) yield p).toVector //Note: this requires the jps command from the jdk on the PATH def kill(pid:Int):Unit = kind match { case Windows => Process(s"taskkill /F /PID $pid").!!; {} case _:Unix => Process(s"kill -9 $pid").!!; {} case _ => println("Warning: The os must be known") } def kill(pid:Pid):Unit = pid.kill def kill(key:String):Unit = ps(key) foreach kill def jkill(key:String):Unit = jps(key) foreach kill //private lazy val toolkit = java.awt.Toolkit.getDefaultToolkit //def isCapsLock = toolkit.getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK) //def isNumLock = toolkit.getLockingKeyState(java.awt.event.KeyEvent.VK_NUM_LOCK) // import Implicit.ec //-- terminal info /**use tput to get the current terminal size http://stackoverflow.com/a/263900/622016 ; only works for unix*/ def terminalSize:Future[Vec] = for( cols <- Shell("tput cols").resultString; rows <- Shell("tput lines").resultString ) yield Vec(cols.toInt,rows.toInt) } object Display{ //def mc = mouseInfo.getDevice.getDefaultConfiguration def currentGC = java.awt.MouseInfo.getPointerInfo.getDevice.getDefaultConfiguration def insets(gc:java.awt.GraphicsConfiguration=currentGC):Display = { val insets = java.awt.Toolkit.getDefaultToolkit.getScreenInsets(gc) val bounds = gc.getBounds val loc = Vec(bounds.x+insets.left, bounds.y+insets.top) val size = Vec( bounds.width - insets.right - insets.left, bounds.height - insets.top - insets.bottom ) Display(loc, size) } def fullScreen(gc:java.awt.GraphicsConfiguration=currentGC):Display = { val bounds = gc.getBounds val loc = Vec(bounds.x, bounds.y) val size = Vec(bounds.width, bounds.height) Display(loc, size) } def allScreens:Display = { val ge = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment val bounds = ge.getScreenDevices.map{_.getDefaultConfiguration.getBounds} val minX = bounds.map{b => b.x}.min val minY = bounds.map{b => b.y}.min val maxX = bounds.map{b => b.x + b.width}.max val maxY = bounds.map{b => b.y + b.height}.max Display(Vec(minX,minY), Vec(maxX-minX, maxY-minY)) } //TODO move this screen shot to the display class so sub rect can be selected import java.awt.image.BufferedImage import java.awt.{Toolkit, Rectangle} private lazy val robot = new java.awt.Robot() def screenshotImage:BufferedImage = robot.createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())); def screenshot(file:File):Try[File] = file.mkParents.flatMap{file => Try{ javax.imageio.ImageIO.write(screenshotImage, file.ext, file); file } } def screenshot:Try[File] = screenshot(file"export/screen-${Date.now.krypton.ms}.png") } case class Display(loc:Vec, size:Vec)