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 }