/* * Copyright 2023 dorkbox, llc * * 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. */ @file:Suppress("unused", "SameParameterValue", "MemberVisibilityCanBePrivate", "LocalVariableName") package dorkbox.os import java.io.BufferedReader import java.io.File import java.io.FileReader import java.util.* import java.util.concurrent.* object OS { /** * Gets the version number. */ const val version = "1.11" init { // Add this project to the updates system, which verifies this class + UUID + version information dorkbox.updates.Updates.add(OS::class.java, "a2afbd7d98084a9eb6eb663570dbec77", version) } // make the default unix val LINE_SEPARATOR = getProperty("line.separator", "\n") const val LINE_SEPARATOR_UNIX = "\n" const val LINE_SEPARATOR_MACOS = "\r" const val LINE_SEPARATOR_WINDOWS = "\r\n" val TEMP_DIR = File(getProperty("java.io.tmpdir", "temp")).absoluteFile /** * The currently running MAJOR java version as a NUMBER. For example, "Java version 1.7u45", and converts it into 7, uses JEP 223 for java > 9 */ val javaVersion: Int by lazy { // We are >= java 10, use JEP 223 to get the version (early releases of 9 might not have JEP 223, so 10 is guaranteed to have it) var fullJavaVersion = getProperty("java.version", "9") if (fullJavaVersion.startsWith("1.")) { when (fullJavaVersion[2]) { '4' -> 4 '5' -> 5 '6' -> 6 '7' -> 7 '8' -> 8 '9' -> 9 else -> 8 } } else { fullJavaVersion = getProperty("java.specification.version", "10") try { // it will ALWAYS be the major release version as an integer. See http://openjdk.java.net/jeps/223 fullJavaVersion.toInt() } catch (ignored: Exception) { // the last valid guess we have, since the current Java implementation, whatever it is, decided not to cooperate with JEP 223. 8 } } } /** * Returns true if the currently running JVM is using the classpath or modules (JPMS) */ val usesJpms = JVM.usesJpms /** * Returns the *ORIGINAL* system time zone, before (*IF*) it was changed to UTC */ val originalTimeZone = TimeZone.getDefault().id!! /** * JVM reported osName, the default (if there is none detected) is 'linux' */ val osName = getProperty("os.name", "linux").lowercase() /** * JVM reported osArch, the default (if there is none detected) is 'amd64' */ val osArch = getProperty("os.arch", "amd64").lowercase() /** * @return the optimum number of threads for a given task. Makes certain not to take ALL the threads, always returns at least one * thread. */ val optimumNumberOfThreads = (Runtime.getRuntime().availableProcessors() - 2).coerceAtLeast(1) /** * The determined OS type */ val type: OSType by lazy { if (osName.startsWith("linux")) { // best way to determine if it's android. // Sometimes java binaries include Android classes on the classpath, even if it isn't actually Android, so we check the VM val isAndroid = "Dalvik" == getProperty("java.vm.name", "") if (isAndroid) { // android check from https://stackoverflow.com/questions/14859954/android-os-arch-output-for-arm-mips-x86 when (osArch) { "armeabi" -> { OSType.AndroidArm56 // old/low-end non-hf 32bit cpu } "armeabi-v7a" -> { OSType.AndroidArm7 // 32bit hf cpu } "arm64-v8a" -> { OSType.AndroidArm8 // 64bit hf cpu } "x86" -> { OSType.AndroidX86 // 32bit x86 (usually emulator) } "x86_64" -> { OSType.AndroidX86_64 // 64bit x86 (usually emulator) } "mips" -> { OSType.AndroidMips // 32bit mips } "mips64" -> { OSType.AndroidMips64 // 64bit mips } else -> { throw java.lang.RuntimeException("Unable to determine OS type for $osName $osArch") } } } else { // http://mail.openjdk.java.net/pipermail/jigsaw-dev/2017-April/012107.html when(osArch) { "i386", "x86" -> { OSType.Linux32 } "arm" -> { OSType.LinuxArm32 } "x86_64", "amd64" -> { OSType.Linux64 } "aarch64" -> { OSType.LinuxArm64 } else -> { when { // oddballs (android usually) osArch.startsWith("arm64") -> { OSType.LinuxArm64 } osArch.startsWith("arm") -> { if (osArch.contains("v8")) { OSType.LinuxArm64 } else { OSType.LinuxArm32 } } else -> { throw java.lang.RuntimeException("Unable to determine OS type for $osName $osArch") } } } } } } else if (osName.startsWith("windows")) { if ("amd64" == osArch) { OSType.Windows64 } else { OSType.Windows32 } } else if (osName.startsWith("mac") || osName.startsWith("darwin")) { when (osArch) { "x86_64" -> { OSType.MacOsX64 } "aarch64" -> { OSType.MacOsArm } else -> { OSType.MacOsX32 // new macOS is no longer 32 bit, but just in case. } } } else if (osName.startsWith("freebsd") || osName.contains("nix") || osName.contains("nux") || osName.startsWith("aix")) { when (osArch) { "x86", "i386" -> { OSType.Unix32 } "arm" -> { OSType.UnixArm } else -> { OSType.Unix64 } } } else if (osName.startsWith("solaris") || osName.startsWith("sunos")) { OSType.Solaris } else { throw java.lang.RuntimeException("Unable to determine OS type for $osName $osArch") } } init { if (!TEMP_DIR.isDirectory) { // create the temp dir if necessary because the TEMP dir doesn't exist. TEMP_DIR.mkdirs() } /* * By default, the timer resolution on Windows ARE NOT high-resolution (16ms vs 1ms) * * 'Thread.sleep(1)' will not really sleep for 1ms, but will really sleep for ~16ms. This long-running sleep will trick Windows * into using higher resolution timers. * * See: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6435126 */ if (type.isWindows) { // only necessary on windows val timerAccuracyThread = Thread( { while (true) { try { Thread.sleep(Long.MAX_VALUE) } catch (ignored: Exception) { } } }, "FixWindowsHighResTimer") timerAccuracyThread.isDaemon = true timerAccuracyThread.start() } } /** * Clears/removes the property from the system properties. */ fun clearProperty(property: String) { System.clearProperty(property) } /** * @return the previous value of the system property, or 'null' if it did not have one. */ fun setProperty(property: String, value: String): String? { return System.setProperty(property, value) } /** * @return the previous value of the system property, or the [defaultValue] if it did not have one. */ fun setProperty(property: String, value: String, defaultValue: String): String { return System.setProperty(property, value) ?: defaultValue } /** * @return the value of the Java system property with the specified `property`, or null if it does not exist. */ fun getProperty(property: String): String? { return System.getProperty(property, null) } /** * @return the value of the Java system property with the specified `property`, while falling back to the * specified default value if the property access fails. */ fun getProperty(property: String, defaultValue: String): String { return System.getProperty(property, defaultValue) } /** * @return the Java system properties in a safe way. */ fun getProperties(): Map { @Suppress("UNCHECKED_CAST") return System.getProperties().toMap() as Map } /** * @return the System Environment property in a safe way for a given property, or null if it does not exist. */ fun getEnv(): Map { return System.getenv() } /** * @return the System Environment property in a safe way for a given property, or null if it does not exist. */ fun getEnv(property: String): String? { return System.getenv(property) } /** * @return the value of the Java system property with the specified `property`, while falling back to the * specified default value if the property access fails. */ fun getEnv(property: String, defaultValue: String): String { return getEnv(property, defaultValue) } /** * @return the value of the Java system property with the specified `property`, while falling back to the * specified default value if the property access fails. */ fun getBoolean(property: String, defaultValue: Boolean): Boolean { var value = getProperty(property) ?: return defaultValue value = value.trim().lowercase(Locale.getDefault()) if (value.isEmpty()) { return defaultValue } if ("false" == value || "no" == value || "0" == value) { return false } return if ("true" == value || "yes" == value || "1" == value) { true } else defaultValue } /** * @return the value of the Java system property with the specified `property`, while falling back to the * specified default value if the property access fails. */ fun getInt(property: String, defaultValue: Int): Int { var value = getProperty(property) ?: return defaultValue value = value.trim() try { return value.toInt() } catch (ignored: Exception) { } return defaultValue } /** * @return the value of the Java system property with the specified `property`, while falling back to the * specified default value if the property access fails. */ fun getLong(property: String, defaultValue: Long): Long { var value = getProperty(property) ?: return defaultValue value = value.trim() try { return value.toLong() } catch (ignored: Exception) { } return defaultValue } /** * @return the value of the Java system property with the specified `property`, while falling back to the * specified default value if the property access fails. */ fun getFloat(property: String, defaultValue: Float): Float { var value = getProperty(property) ?: return defaultValue value = value.trim() try { return value.toFloat() } catch (ignored: Exception) { } return defaultValue } /** * @return the value of the Java system property with the specified `property`, while falling back to the * specified default value if the property access fails. */ fun getDouble(property: String, defaultValue: Double): Double { var value = getProperty(property) ?: return defaultValue value = value.trim() try { return value.toDouble() } catch (ignored: Exception) { } return defaultValue } val is32bit = type.is32bit val is64bit = type.is64bit /** * @return true if this is x86/x64/arm architecture (intel/amd/etc) processor. */ val isX86 = type.isX86 val isMips = type.isMips val isArm = type.isArm val isLinux = type.isLinux val isUnix = type.isUnix val isSolaris = type.isSolaris val isWindows = type.isWindows val isMacOsX = type.isMacOsX val isAndroid = type.isAndroid /** * Set our system to UTC time zone. Retrieve the **original** time zone via [.getOriginalTimeZone] */ fun setUTC() { // have to set our default timezone to UTC. EVERYTHING will be UTC, and if we want local, we must explicitly ask for it. TimeZone.setDefault(TimeZone.getTimeZone("UTC")) } /** * @return the first line of the exception message from 'throwable', or the type if there was no message. */ fun getExceptionMessage(throwable: Throwable): String? { var message = throwable.message if (message != null) { val index = message.indexOf(LINE_SEPARATOR) if (index > -1) { message = message.substring(0, index) } } else { message = throwable.javaClass.simpleName } return message } /** * Executes the given command and returns its output. * * This is based on an aggregate of the answers provided here: [https://stackoverflow.com/questions/35421699/how-to-invoke-external-command-from-within-kotlin-code] */ private fun execute(vararg args: String, timeout: Long = 60): String { val process = ProcessBuilder(args.toList()) .redirectOutput(ProcessBuilder.Redirect.PIPE) .redirectError(ProcessBuilder.Redirect.PIPE) .start() val text = process.inputStream.bufferedReader().readText().trim() process.waitFor(timeout, TimeUnit.SECONDS) return text } // true if the exit code is 0 (meaning standard exit) private fun executeStatus(vararg args: String, timeout: Long = 60): Boolean { return ProcessBuilder(args.toList()) .redirectOutput(ProcessBuilder.Redirect.PIPE) .redirectError(ProcessBuilder.Redirect.PIPE) .start() .apply { waitFor(timeout, TimeUnit.SECONDS) } .exitValue() == 0 } object Windows { /** * Version info at release. ``` https://en.wikipedia.org/wiki/Comparison_of_Microsoft_Windows_versions Windows XP 5.1.2600 (2001-10-25) Windows Server 2003 5.2.3790 (2003-04-24) Windows Home Server 5.2.3790 (2007-06-16) ------------------------------------------------- Windows Vista 6.0.6000 (2006-11-08) Windows Server 2008 SP1 6.0.6001 (2008-02-27) Windows Server 2008 SP2 6.0.6002 (2009-04-28) ------------------------------------------------- Windows 7 6.1.7600 (2009-10-22) Windows Server 2008 R2 6.1.7600 (2009-10-22) Windows Server 2008 R2 SP1 6.1.7601 (?) Windows Home Server 2011 6.1.8400 (2011-04-05) ------------------------------------------------- Windows 8 6.2.9200 (2012-10-26) Windows Server 2012 6.2.9200 (2012-09-04) ------------------------------------------------- Windows 8.1 6.3.9600 (2013-10-18) Windows Server 2012 R2 6.3.9600 (2013-10-18) ------------------------------------------------- Windows 10 10.0.10240 (2015-07-29) Windows 10 10.0.10586 (2015-11-12) Windows 10 10.0.14393 (2016-07-18) Windows Server 2016 10.0.14393 (2016-10-12) Windows Server 2019 10.0.17763 (2018-10-02) Windows Server 2022 10.0.20348 (2021-08-18) Windows 11 Original Release 10.0.22000 (2021-10-05) Windows 11 2022 Update 10.0.22621 (2022-09-20) ``` * @return the {major}{minor} version of windows, ie: Windows Version 10.0.10586 -> {10}{0} */ val version: IntArray by lazy { if (!isWindows) { intArrayOf(0, 0, 0) } else { val version = IntArray(3) try { val output = execute("cmd.exe", "/c", "ver") if (output.isNotEmpty()) { // OF NOTE: It is possible to have a different encoding of windows, where the word "Version" doesn't exist. // in this case, we'll just take the next set of numbers available // Microsoft Windows [Version 10.0.22000.2600] // Microsoft Windows [??? 10.0.22621.2283] (different encoding) // slice out the [] because we don't want to include windows product names! (like Windows 2012) in the name! // I don't specifically have this version to test, however it is easy enough to guard against. val shortenedOutput = output.substring(output.indexOf("[") + 1, output.indexOf("]")) val index = shortenedOutput.indexOfFirst { it.isDigit() } val versionInfoOnly = shortenedOutput.substring(index, shortenedOutput.length) val split = versionInfoOnly.split(".").toTypedArray() if (split.size == 4) { version[0] = split[0].toInt() version[1] = split[1].toInt() version[2] = split[2].toInt() } } } catch (ignored: Throwable) { } version } } /** * @return is Windows XP or equivalent */ val isWindowsXP = version[0] == 5 /** * @return is Windows Vista or equivalent */ val isWindowsVista = version[0] == 6 && version[1] == 0 /** * @return is Windows 7 or equivalent */ val isWindows7 = version[0] == 6 && version[1] == 1 /** * @return is Windows 8 or equivalent */ val isWindows8 = version[0] == 6 && version[1] == 2 /** * @return is Windows 8.1 or equivalent */ val isWindows8_1 = version[0] == 6 && version[1] == 3 /** * @return is greater than or equal to Windows 8.1 or equivalent */ val isWindows8_1_plus: Boolean by lazy { val version = version if (version[0] == 6 && version[1] >= 3) { true } else { version[0] > 6 } } /** * @return is Windows 10 or equivalent */ val isWindows10 = version[0] == 10 /** * @return is Windows 10 or greater */ val isWindows10_plus = version[0] >= 10 /** * @return is Windows 11 (original release was 21H2) */ val isWindows11 = version[0] == 10 && version[1] == 0 && version[2] >= 22000 /** * @return is Windows 11 update 22H2 */ val isWindows11_22H2 = version[0] == 10 && version[1] == 0 && version[2] >= 22621 } object Unix { // uname val isFreeBSD: Boolean by lazy { if (!isUnix) { false } else { try { // uname execute("uname").startsWith("FreeBSD") } catch (ignored: Throwable) { false } } } } object Linux { // NAME="Arch Linux" // PRETTY_NAME="Arch Linux" // ID=arch // ID_LIKE=archlinux // ANSI_COLOR="0;36" // HOME_URL="https://www.archlinux.org/" // SUPPORT_URL="https://bbs.archlinux.org/" // BUG_REPORT_URL="https://bugs.archlinux.org/" // similar on other distro's. ID is always the "key" to the distro // this is likely a file we are interested in.// looking for files like /etc/os-release /** * @return os release info or "" */ val info: String by lazy { if (!isLinux) { "" } else { var data = "" try { val releaseFiles: MutableList = LinkedList() var totalLength = 0 // looking for files like /etc/os-release val file = File("/etc") if (file.isDirectory) { val list = file.listFiles() if (list != null) { for (f in list) { if (f.isFile && f.name.contains("release")) { // this is likely a file we are interested in. releaseFiles.add(f) totalLength += file.length().toInt() } } } } if (totalLength > 0) { val fileContents = StringBuilder(totalLength) for (releaseFile in releaseFiles) { BufferedReader(FileReader(releaseFile)).use { reader -> var currentLine: String? // NAME="Arch Linux" // PRETTY_NAME="Arch Linux" // ID=arch // ID_LIKE=archlinux // ANSI_COLOR="0;36" // HOME_URL="https://www.archlinux.org/" // SUPPORT_URL="https://bbs.archlinux.org/" // BUG_REPORT_URL="https://bugs.archlinux.org/" // similar on other distro's. ID is always the "key" to the distro while (reader.readLine().also { currentLine = it } != null) { fileContents.append(currentLine).append(LINE_SEPARATOR_UNIX) } } } data = fileContents.toString() } } catch (ignored: Throwable) { } data } } /** * @param id the info ID to check, ie: ubuntu, arch, debian, etc... This is what the OS vendor uses to ID their OS. * * @return true if this OS is identified as the specified ID. */ fun isReleaseType(id: String): Boolean { // also matches on 'DISTRIB_ID' and 'VERSION_ID' // ID=linuxmint/fedora/arch/ubuntu/etc return info.contains("ID=$id\n") } val isArch: Boolean by lazy { isReleaseType("arch") } val isDebian: Boolean by lazy { isReleaseType("debian") } val isElementaryOS: Boolean by lazy { try { // ID="elementary" (notice the extra quotes) info.contains("ID=\"elementary\"\n") || info.contains("ID=elementary\n") || // this is specific to eOS < 0.3.2 info.contains("ID=\"elementary OS\"\n") } catch (ignored: Throwable) { false } } val isFedora: Boolean by lazy { isReleaseType("fedora") } val fedoraVersion: Int by lazy { if (!isFedora) { 0 } else { try { // ID=fedora if (info.contains("ID=fedora\n")) { // should be: VERSION_ID=23\n or something val beginIndex = info.indexOf("VERSION_ID=") + 11 val fedoraVersion_ = info.substring(beginIndex, info.indexOf(LINE_SEPARATOR_UNIX, beginIndex)) fedoraVersion_.toInt() } else { 0 } } catch (ignored: Throwable) { 0 } } } val isLinuxMint: Boolean by lazy { isReleaseType("linuxmint") } val isUbuntu: Boolean by lazy { isReleaseType("ubuntu") } val ubuntuVersion: IntArray by lazy { @Suppress("DuplicatedCode") if (!isUbuntu) { intArrayOf(0, 0) } else if (distribReleaseInfo != null) { val split = distribReleaseInfo!!.split(".").toTypedArray() intArrayOf(split[0].toInt(), split[1].toInt()) } else { intArrayOf(0, 0) } } val elementaryOSVersion: IntArray by lazy { // 0.1 Jupiter. The first stable version of elementary OS was Jupiter, published on 31 March 2011 and based on Ubuntu 10.10. ... // 0.2 Luna. elementary OS 0.2 "Luna" ... // 0.3 Freya. elementary OS 0.3 "Freya" ... // 0.4 Loki. elementary OS 0.4, known by its codename, "Loki", was released on 9 September 2016. ... // 5.0 Juno @Suppress("DuplicatedCode") if (!isElementaryOS) { intArrayOf(0, 0) } else if (distribReleaseInfo != null) { val split = distribReleaseInfo!!.split(".").toTypedArray() intArrayOf(split[0].toInt(), split[1].toInt()) } else { intArrayOf(0, 0) } } val isKali: Boolean by lazy { isReleaseType("kali") } val isPop: Boolean by lazy { isReleaseType("pop") } val isIgel: Boolean by lazy { isReleaseType("IGEL") } /** * @return the `DISTRIB_RELEASE` info as a String, if possible. Otherwise NULL */ val distribReleaseInfo: String? by lazy { val releaseString = "DISTRIB_RELEASE=" var index = info.indexOf(releaseString) var data: String? = null try { if (index > -1) { index += releaseString.length val newLine = info.indexOf(LINE_SEPARATOR_UNIX, index) if (newLine > index) { data = info.substring(index, newLine) } } } catch (ignored: Throwable) { } data } val isWSL: Boolean by lazy { try { // looking for /proc/version val file = File("/proc/version") var data: Boolean? = null if (file.canRead()) { try { val msString: Boolean BufferedReader(FileReader(file)).use { reader -> // Linux version 4.4.0-19041-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #488-Microsoft Mon Sep 01 13:43:00 PST 2020 msString = reader.readLine().contains("-Microsoft") } data = msString } catch (ignored: Throwable) { } } if (data == null) { // reading the file didn't work for whatever reason... // uname -v data = execute("/usr/bin/uname", "-v").contains("-Microsoft") } if (data == true) { data } else { false } } catch (ignored: Throwable) { false } } val isRoot: Boolean by lazy { // this means we are running as sudo var isSudoOrRoot = System.getenv("SUDO_USER") != null if (!isSudoOrRoot) { // running as root (also can be "sudo" user). A lot slower that checking a sys env, but this is guaranteed to work try { // id -u isSudoOrRoot = "0" == execute("/usr/bin/id", "-u") } catch (ignored: Throwable) { } } isSudoOrRoot } object PackageManager { enum class Type(val installString: String) { APT("apt install"), APTGET("apt-get install"), YUM("yum install"), PACMAN("pacman -S "); } val type: Type by lazy { if (File("/usr/bin/apt").canExecute()) { Type.APT } else if (File("/usr/bin/apt-get").canExecute()) { Type.APTGET } else if (File("/usr/bin/yum").canExecute()) { Type.YUM } else if (File("/usr/bin/pacman").canExecute()) { Type.PACMAN } else { Type.APTGET } // default is apt-get, even if it isn't correct } /** * @return true if the package is installed */ fun isPackageInstalled(packageName: String): Boolean { // dpkg // dpkg -L libappindicator3 // dpkg-query: package 'libappindicator3' is not installed val is_dpkg = File("/usr/bin/dpkg").canExecute() if (is_dpkg) { return !execute("/usr/bin/dpkg", "-L", packageName).contains("is not installed") } // rpm // rpm -q libappindicator234 // package libappindicator234 is not installed val is_rpm = File("/usr/bin/rpm").canExecute() if (is_rpm) { return !execute("/usr/bin/rpm", "-q", packageName).contains("is not installed") } // pacman // pacman -Qi val is_pacmac = File("/usr/bin/pacman").canExecute() if (is_pacmac) { try { // use the exit code to determine if the packages exists on the system // 0 the package exists, 1 it doesn't return executeStatus("/usr/bin/pacman", "-Qi", packageName) //return start == 0 } catch (ignored: Exception) { } } return false } } } object DesktopEnv { enum class Env { Gnome, KDE, Unity, Unity7, XFCE, LXDE, MATE, Pantheon, ChromeOS, Unknown } enum class EnvType { X11, WAYLAND, Unknown } private fun isValidCommand(partialExpectationInOutput: String, commandOutput: String): Boolean { return (commandOutput.contains(partialExpectationInOutput) && !commandOutput.contains("not installed") && !commandOutput.contains("No such file or directory")) } // have no idea how this can happen.... val type: EnvType by lazy { when (getEnv("XDG_SESSION_TYPE")) { "x11" -> { EnvType.X11 } "wayland" -> { EnvType.WAYLAND } else -> { EnvType.Unknown } } } val isX11 = type == EnvType.X11 val isWayland = type == EnvType.WAYLAND val isMATE: Boolean by lazy { if (!isLinux && !isUnix) { false } else { try { File("/usr/bin/mate-about").exists() } catch (ignored: Throwable) { false } } } val isGnome: Boolean by lazy { if (!isLinux && !isUnix) { false } else { try { // note: some versions of linux can ONLY access "ps a"; FreeBSD and most linux is "ps x" // we try "x" first // ps x | grep gnome-shell var contains = execute("/usr/bin/ps", "x").contains("gnome-shell") if (!contains && isLinux) { // only try again if we are linux // ps a | grep gnome-shell contains = execute("/usr/bin/ps", "a").contains("gnome-shell") } contains } catch (ignored: Throwable) { false } } } /** * @return a string representing the current gnome-shell version, or NULL if it could not be found */ val gnomeVersion: String? by lazy { if (!isLinux && !isUnix) { null } else { try { // gnome-shell --version val versionString = execute("/usr/bin/gnome-shell", "--version") if (versionString.isNotEmpty()) { // GNOME Shell 3.14.1 val version = versionString.replace("[^\\d.]".toRegex(), "") if (version.isNotEmpty() && version.indexOf('.') > 0) { // should just be 3.14.1 or 3.20 or similar version } else { null } } else { null } } catch (ignored: Throwable) { null } } } // Check if plasmashell is running, if it is -- then we are most likely KDE val isKDE: Boolean by lazy { val XDG = getEnv("XDG_CURRENT_DESKTOP") if (XDG == null) { // Check if plasmashell is running, if it is -- then we are most likely KDE plasmaVersionFull != null && !plasmaVersionFull!!.startsWith("0") } else { "kde".equals(XDG, ignoreCase = true) } } /** * The full version number of plasma shell (if running) as a String. * * @return cannot represent '5.6.5' as a number, so we return a String instead or NULL if unknown */ val plasmaVersionFull: String? by lazy { if (!isLinux && !isUnix) { null } else { try { // plasma-desktop -v // plasmashell --version val output = execute("/usr/bin/plasmashell", "--version") if (output.isNotEmpty()) { // DEFAULT icon size is 16. KDE is bananas on what they did with tray icon scale // should be: plasmashell 5.6.5 or something val s = "plasmashell " if (isValidCommand(s, output)) { output.substring(output.indexOf(s) + s.length) } else { null } } else { null } } catch (ignored: Throwable) { null } } } val isXfce: Boolean by lazy { if (!isLinux && !isUnix) { false } else { try { // note: some versions of linux can ONLY access "ps a"; FreeBSD and most linux is "ps x" // we try "x" first // ps x | grep xfce var contains = execute("/usr/bin/ps", "x").contains("xfce") if (!contains && isLinux) { // only try again if we are linux // ps a | grep gnome-shell contains = execute("/usr/bin/ps", "a").contains("xfce") } contains } catch (ignored: Throwable) { false } } } /** * There are sometimes problems with nautilus (the file browser) and some GTK methods. It is ridiculous for me to have to * work around their bugs like this. * * see: https://askubuntu.com/questions/788182/nautilus-not-opening-up-showing-glib-error */ val isNautilus: Boolean by lazy { if (!isLinux && !isUnix) { false } else { try { // nautilus --version val output = execute("/usr/bin/nautilus", "--version") if (output.isNotEmpty()) { // should be: GNOME nautilus 3.14.3 or something val s = "GNOME nautilus " isValidCommand(s, output) } else { false } } catch (ignored: Throwable) { false } } } val isChromeOS: Boolean by lazy { if (!isLinux) { false } else { try { // ps aux | grep chromeos execute("/usr/bin/ps", "aux").contains("chromeos") } catch (ignored: Throwable) { false } } } /** * @param channel which XFCE channel to query. Cannot be null * @param property which property (in the channel) to query. Null will list all properties in the channel * * @return the property value or "". */ fun queryXfce(channel: String, property: String?): String { if (!isLinux && !isUnix) { return "" } try { // xfconf-query -c xfce4-panel -l val commands: MutableList = ArrayList() commands.add("/usr/bin/xfconf-query") commands.add("-c") commands.add(channel) if (property != null) { // get property for channel commands.add("-p") commands.add(property) } else { // list all properties for the channel commands.add("-l") } return execute(*commands.toTypedArray()) } catch (ignored: Throwable) { return "" } } val env: Env by lazy { // if we are running as ROOT, we *** WILL NOT *** have access to 'XDG_CURRENT_DESKTOP' // *unless env's are preserved, but they are not guaranteed to be // see: http://askubuntu.com/questions/72549/how-to-determine-which-window-manager-is-running var XDG = getEnv("XDG_CURRENT_DESKTOP") if (XDG == null) { // maybe we are running as root??? XDG = "unknown" // try to autodetect if we should use app indicator or gtkstatusicon } // Ubuntu 17.10+ is special ... this is ubuntu:GNOME (it now uses wayland instead of x11, so many things have changed...) // So it's gnome, and gnome-shell, but with some caveats // see: https://bugs.launchpad.net/ubuntu/+source/gnome-shell/+bug/1700465 // BLEH. if gnome-shell is running, IT'S REALLY GNOME! // we must ALWAYS do this check!! if (isGnome) { XDG = "gnome" } else if (isKDE) { // same thing with plasmashell! XDG = "kde" } else if (isXfce) { // https://github.com/dorkbox/SystemTray/issues/100 // IGEL linux doesn't say what it is... but we know it's XFCE ... EVEN THOUGH it reports X11!! XDG = "xfce" } if ("unity".equals(XDG, ignoreCase = true)) { // Ubuntu Unity is a weird combination. It's "Gnome", but it's not "Gnome Shell". Env.Unity } else if ("unity:unity7".equals(XDG, ignoreCase = true)) { // Ubuntu Unity7 is a weird combination. It's "Gnome", but it's not "Gnome Shell". Env.Unity7 } else if ("xfce".equals(XDG, ignoreCase = true)) { Env.XFCE } else if ("lxde".equals(XDG, ignoreCase = true)) { Env.LXDE } else if ("kde".equals(XDG, ignoreCase = true)) { Env.KDE } else if ("pantheon".equals(XDG, ignoreCase = true)) { Env.Pantheon } else if ("gnome".equals(XDG, ignoreCase = true)) { Env.Gnome } else if (isChromeOS) { // maybe it's chromeOS? Env.ChromeOS } else if (isMATE) { Env.MATE } else { Env.Unknown } } val isUnity = isUnity(env) fun isUnity(env: Env): Boolean { return env == Env.Unity || env == Env.Unity7 } } }