1 /++ 2 Functions that deal with OS- and/or platform-specifics. 3 4 See_Also: 5 [kameloso.terminal] 6 7 Copyright: [JR](https://github.com/zorael) 8 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 9 10 Authors: 11 [JR](https://github.com/zorael) 12 +/ 13 module kameloso.platform; 14 15 private: 16 17 import std.process : Pid; 18 19 public: 20 21 @safe: 22 23 24 // currentPlatform 25 /++ 26 Returns the string of the name of the current platform, adjusted to include 27 `cygwin` as an alternative next to `win32` and `win64`, as well as embedded 28 terminal consoles like in Visual Studio Code. 29 30 Example: 31 --- 32 switch (currentPlatform) 33 { 34 case "Cygwin": 35 case "vscode": 36 // Special code for the terminal not being a conventional terminal 37 // (instead acting like a pager) 38 break; 39 40 default: 41 // Code for normal terminal 42 break; 43 } 44 --- 45 46 Returns: 47 String name of the current platform. 48 +/ 49 auto currentPlatform() 50 { 51 import lu.conv : Enum; 52 import std.process : environment; 53 import std.system : os; 54 55 enum osName = Enum!(typeof(os)).toString(os); 56 57 version(Windows) 58 { 59 immutable term = environment.get("TERM", string.init); 60 61 if (term.length) 62 { 63 try 64 { 65 import std.process : execute; 66 67 // Get the uname and strip the newline 68 static immutable unameCommand = [ "uname", "-o" ]; 69 immutable uname = execute(unameCommand).output; 70 return uname.length ? uname[0..$-1] : osName; 71 } 72 catch (Exception _) 73 { 74 return osName; 75 } 76 } 77 else 78 { 79 return osName; 80 } 81 } 82 else 83 { 84 return environment.get("TERM_PROGRAM", osName); 85 } 86 } 87 88 89 // configurationBaseDirectory 90 /++ 91 Divines the default configuration file base directory, depending on what 92 platform we're currently running. 93 94 On non-macOS Posix it defaults to `$XDG_CONFIG_HOME` and falls back to 95 `~/.config` if no `$XDG_CONFIG_HOME` environment variable present. 96 97 On macOS it defaults to `$HOME/Library/Application Support`. 98 99 On Windows it defaults to `%APPDATA%`. 100 101 Returns: 102 A string path to the default configuration file. 103 +/ 104 auto configurationBaseDirectory() 105 { 106 import std.process : environment; 107 108 version(OSX) 109 { 110 import std.path : buildNormalizedPath; 111 return buildNormalizedPath( 112 environment["HOME"], 113 "Library", 114 "Preferences"); 115 } 116 else version(Posix) 117 { 118 import std.path : expandTilde; 119 120 // Assume XDG 121 enum defaultDir = "~/.config"; 122 return environment.get("XDG_CONFIG_HOME", defaultDir).expandTilde; 123 } 124 else version(Windows) 125 { 126 // Blindly assume %APPDATA% is defined 127 return environment["APPDATA"]; 128 } 129 else 130 { 131 static assert(0, "Unsupported platform, please file a bug."); 132 } 133 } 134 135 /// 136 unittest 137 { 138 import std.algorithm.searching : endsWith; 139 140 immutable cfgd = configurationBaseDirectory; 141 142 version(OSX) 143 { 144 assert(cfgd.endsWith("Library/Preferences"), cfgd); 145 } 146 else version(Posix) 147 { 148 import std.process : environment; 149 150 environment["XDG_CONFIG_HOME"] = "/tmp"; 151 immutable cfgdTmp = configurationBaseDirectory; 152 assert((cfgdTmp == "/tmp"), cfgdTmp); 153 154 environment.remove("XDG_CONFIG_HOME"); 155 immutable cfgdWithout = configurationBaseDirectory; 156 assert(cfgdWithout.endsWith("/.config"), cfgdWithout); 157 } 158 else version(Windows) 159 { 160 assert(cfgd.endsWith("\\Roaming"), cfgd); 161 } 162 } 163 164 165 // resourceBaseDirectory 166 /++ 167 Divines the default resource base directory, depending on what platform 168 we're currently running. 169 170 On non-macOS Posix it defaults to `$XDG_DATA_HOME` and falls back to 171 `$HOME/.local/share` if no `$XDG_DATA_HOME` environment variable present. 172 173 On macOS it defaults to `$HOME/Library/Application Support`. 174 175 On Windows it defaults to `%LOCALAPPDATA%`. 176 177 Returns: 178 A string path to the default resource base directory. 179 +/ 180 auto resourceBaseDirectory() 181 { 182 import std.process : environment; 183 184 version(OSX) 185 { 186 import std.path : buildNormalizedPath; 187 return buildNormalizedPath( 188 environment["HOME"], 189 "Library", 190 "Application Support"); 191 } 192 else version(Posix) 193 { 194 import std.path : expandTilde; 195 enum defaultDir = "~/.local/share"; 196 return environment.get("XDG_DATA_HOME", defaultDir).expandTilde; 197 } 198 else version(Windows) 199 { 200 // Blindly assume %LOCALAPPDATA% is defined 201 return environment["LOCALAPPDATA"]; 202 } 203 else 204 { 205 static assert(0, "Unsupported platform, please file a bug."); 206 } 207 } 208 209 /// 210 unittest 211 { 212 import std.algorithm.searching : endsWith; 213 214 version(OSX) 215 { 216 immutable rbd = resourceBaseDirectory; 217 assert(rbd.endsWith("Library/Application Support"), rbd); 218 } 219 else version(Posix) 220 { 221 import lu.string : beginsWith; 222 import std.process : environment; 223 224 environment["XDG_DATA_HOME"] = "/tmp"; 225 string rbd = resourceBaseDirectory; 226 assert((rbd == "/tmp"), rbd); 227 228 environment.remove("XDG_DATA_HOME"); 229 rbd = resourceBaseDirectory; 230 assert(rbd.beginsWith("/home/") && rbd.endsWith("/.local/share")); 231 } 232 else version(Windows) 233 { 234 immutable rbd = resourceBaseDirectory; 235 assert(rbd.endsWith("\\Local"), rbd); 236 } 237 } 238 239 240 // openInBrowser 241 /++ 242 Opens up the passed URL in a web browser. 243 244 Params: 245 url = URL to open. 246 247 Returns: 248 A [std.process.Pid|Pid] of the spawned process. Remember to [std.process.wait|wait]. 249 250 Throws: 251 [object.Exception|Exception] if there were no `DISPLAY` environment 252 variable on non-macOS Posix platforms, indicative of no X.org server or 253 Wayland compositor running. 254 +/ 255 auto openInBrowser(const string url) 256 { 257 import std.stdio : File; 258 259 version(Posix) 260 { 261 import std.process : ProcessException, environment, spawnProcess; 262 263 version(OSX) 264 { 265 enum open = "open"; 266 } 267 else 268 { 269 // Assume XDG 270 enum open = "xdg-open"; 271 272 if (!environment.get("DISPLAY", string.init).length && 273 !environment.get("WAYLAND_DISPLAY", string.init).length) 274 { 275 throw new Exception("No graphical interface detected"); 276 } 277 } 278 279 immutable browserExecutable = environment.get("BROWSER", open); 280 string[2] browserCommand = [ browserExecutable, url ]; // mutable 281 auto devNull = File("/dev/null", "r+"); 282 283 try 284 { 285 return spawnProcess(browserCommand[], devNull, devNull, devNull); 286 } 287 catch (ProcessException e) 288 { 289 if (browserExecutable == open) throw e; 290 291 browserCommand[0] = open; 292 return spawnProcess(browserCommand[], devNull, devNull, devNull); 293 } 294 } 295 else version(Windows) 296 { 297 import std.file : tempDir; 298 import std.format : format; 299 import std.path : buildPath; 300 import std.process : spawnProcess; 301 302 enum urlBasename = "kameloso-browser.url"; 303 immutable urlFileName = buildPath(tempDir, urlBasename); 304 305 { 306 auto urlFile = File(urlFileName, "w"); 307 urlFile.writeln("[InternetShortcut]\nURL=", url); 308 } 309 310 immutable string[2] browserCommand = [ "explorer", urlFileName ]; 311 auto nulFile = File("NUL", "r+"); 312 return spawnProcess(browserCommand[], nulFile, nulFile, nulFile); 313 } 314 else 315 { 316 static assert(0, "Unsupported platform, please file a bug."); 317 } 318 } 319 320 321 // execvp 322 /++ 323 Re-executes the program. 324 325 Filters out any captive `--set twitch.*` keygen settings from the 326 arguments originally passed to the program, then calls 327 [std.process.execvp|execvp]. 328 329 On Windows, the behaviour is faked using [std.process.spawnProcess|spawnProcess]. 330 331 Params: 332 args = Arguments passed to the program. 333 334 Returns: 335 On Windows, a [std.process.Pid|Pid] of the spawned process. 336 On Posix, it either exits the program or it throws. 337 338 Throws: 339 On Posix, [ExecException] on failure. 340 On Windows, [std.process.ProcessException|ProcessException] on failure. 341 +/ 342 Pid execvp(/*const*/ string[] args) @system 343 { 344 import kameloso.common : logger; 345 import std.algorithm.comparison : among; 346 347 if (args.length > 1) 348 { 349 size_t[] toRemove; 350 351 for (size_t i=1; i<args.length; ++i) 352 { 353 import lu.string : beginsWith, nom; 354 import std.typecons : Flag, No, Yes; 355 356 if (args[i] == "--set") 357 { 358 if (args.length <= i+1) continue; // should never happen 359 360 string slice = args[i+1]; // mutable 361 362 if (slice.beginsWith("twitch.")) 363 { 364 immutable setting = slice.nom!(Yes.inherit)('='); 365 366 if (setting.among!( 367 "twitch.keygen", 368 "twitch.superKeygen", 369 "twitch.googleKeygen", 370 "twitch.spotifyKeygen")) 371 { 372 toRemove ~= i; 373 toRemove ~= i+1; 374 ++i; // Skip next entry 375 } 376 } 377 } 378 else 379 { 380 string slice = args[i]; // mutable 381 immutable setting = slice.nom!(Yes.inherit)('='); 382 383 if (setting.among!( 384 "--setup-twitch", 385 "--get-cacert", 386 "--get-openssl")) 387 { 388 toRemove ~= i; 389 } 390 } 391 } 392 393 foreach (immutable i; toRemove) 394 { 395 import std.algorithm.mutation : SwapStrategy, remove; 396 args = args.remove!(SwapStrategy.stable)(i); 397 } 398 } 399 400 version(Posix) 401 { 402 import std.process : execvp; 403 404 immutable retval = execvp(args[0], args); 405 406 // If we're here, the call failed 407 enum message = "execvp failed"; 408 throw new ExecException(message, retval); 409 } 410 else version(Windows) 411 { 412 import lu.string : beginsWith; 413 import std.array : Appender; 414 import std.process : ProcessException, spawnProcess; 415 416 Appender!(char[]) sink; 417 sink.reserve(128); 418 419 string arg0 = args[0]; // mutable 420 args = args[1..$]; // pop it 421 422 if (arg0.beginsWith('.') || arg0.beginsWith('/')) 423 { 424 // Seems to be a full path 425 } 426 else if ((arg0.length > 3) && (arg0[1] == ':')) 427 { 428 // May be C:\kameloso.exe and would as such be okay 429 } 430 else 431 { 432 // Powershell won't call binaries in the working directory without ./ 433 arg0 = "./" ~ arg0; 434 } 435 436 for (size_t i; i<args.length; ++i) 437 { 438 import std.format : formattedWrite; 439 440 if (sink.data.length) sink.put(' '); 441 442 if ((args.length >= i+1) && 443 args[i].among!( 444 "-H", 445 "-C", 446 "--homeChannels", 447 "--guestChannels", 448 "--set")) 449 { 450 // Octothorpes must be encased in single quotes 451 sink.formattedWrite("%s '%s'", args[i], args[i+1]); 452 ++i; 453 } 454 else 455 { 456 sink.put(args[i]); 457 } 458 } 459 460 const commandLine = 461 [ 462 "cmd.exe", 463 "/c", 464 "start", 465 "/min", 466 "powershell", 467 "-c" 468 ] ~ arg0 ~ sink.data.idup; 469 return spawnProcess(commandLine); 470 } 471 else 472 { 473 static assert(0, "Unsupported platform, please file a bug."); 474 } 475 } 476 477 478 // ExecException 479 /++ 480 Exception thrown when an [std.process.execvp|execvp] action failed. 481 +/ 482 final class ExecException : Exception 483 { 484 /++ 485 [std.process.execvp|execvp] return value. 486 +/ 487 int retval; 488 489 /// Constructor attaching a return value. 490 this( 491 const string msg, 492 const int retval, 493 const string file = __FILE__, 494 const size_t line = __LINE__, 495 Throwable nextInChain = null) pure nothrow @nogc @safe 496 { 497 this.retval = retval; 498 super(msg, file, line, nextInChain); 499 } 500 501 /// Passthrough constructor. 502 this( 503 const string msg, 504 const string file = __FILE__, 505 const size_t line = __LINE__, 506 Throwable nextInChain = null) pure nothrow @nogc @safe 507 { 508 super(msg, file, line, nextInChain); 509 } 510 }