Skip to content

Commit 1110086

Browse files
authored
Avoid creating external processes to read OS version info (#1240)
We are spending about 45ms on macOS to read version information by shelling out to `sw_vers` on each run of GCM. Let's try and be smarter and use BCL APIs, system APIs or file-based sources for the OS and distribution information before resorting to external processes. In my testing, on macOS, we went from 45ms to 4.5ms (10x speedup). Note that we are now also getting something a little bit more useful for Linux: Before: ``` $ GCM_TRACE=1 ./git-credential-manager --version 20:55:36.776385 ...re/Application.cs:95 trace: [RunInternalAsync] Version: 2.1.1.0 20:55:36.787669 ...re/Application.cs:96 trace: [RunInternalAsync] Runtime: .NET 6.0.16 20:55:36.787711 ...re/Application.cs:97 trace: [RunInternalAsync] Platform: Linux (x86-64) 20:55:36.787749 ...re/Application.cs:98 trace: [RunInternalAsync] OSVersion: Linux ubuntu2204-vm1 5.15.0-1037-azure #44-Ubuntu SMP Thu Apr 20 13:19:31 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux .. ``` After: ``` $ GCM_TRACE=1 ./git-credential-manager --version 20:52:12.325628 ...re/Application.cs:95 trace: [RunInternalAsync] Version: 2.1.1.0 20:52:12.337234 ...re/Application.cs:96 trace: [RunInternalAsync] Runtime: .NET 6.0.16 20:52:12.337283 ...re/Application.cs:97 trace: [RunInternalAsync] Platform: Linux (x86-64) 20:52:12.337301 ...re/Application.cs:98 trace: [RunInternalAsync] OSVersion: Ubuntu 22.04.1 LTS .. ```
2 parents 00657e0 + 1288245 commit 1110086

File tree

1 file changed

+86
-30
lines changed

1 file changed

+86
-30
lines changed

src/shared/Core/PlatformUtils.cs

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
34
using System.IO;
5+
using System.Linq;
46
using System.Runtime.InteropServices;
57
using GitCredentialManager.Interop.Posix.Native;
68

@@ -349,52 +351,106 @@ private static string GetOSType()
349351
return "Unknown";
350352
}
351353

354+
private static string _linuxDistroVersion;
355+
352356
private static string GetOSVersion(ITrace2 trace2)
353357
{
358+
//
359+
// Since .NET 5 we can use Environment.OSVersion because it was updated to
360+
// return the correct version on Windows & macOS, rather than the manifested
361+
// version for Windows or the kernel version for macOS.
362+
// https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/environment-osversion-returns-correct-version
363+
//
364+
// However, we still need to use the old method for Windows on .NET Framework
365+
// and call into the Win32 API to get the correct version (regardless of app
366+
// compatibility settings).
367+
#if NETFRAMEWORK
354368
if (IsWindows() && RtlGetVersionEx(out RTL_OSVERSIONINFOEX osvi) == 0)
355369
{
356370
return $"{osvi.dwMajorVersion}.{osvi.dwMinorVersion} (build {osvi.dwBuildNumber})";
357371
}
358-
359-
if (IsMacOS())
372+
#endif
373+
if (IsWindows() || IsMacOS())
360374
{
361-
var psi = new ProcessStartInfo
362-
{
363-
FileName = "/usr/bin/sw_vers",
364-
Arguments = "-productVersion",
365-
RedirectStandardOutput = true
366-
};
367-
368-
using (var swvers = new ChildProcess(trace2, psi))
369-
{
370-
swvers.Start(Trace2ProcessClass.Other);
371-
swvers.WaitForExit();
372-
373-
if (swvers.ExitCode == 0)
374-
{
375-
return swvers.StandardOutput.ReadToEnd().Trim();
376-
}
377-
}
375+
return Environment.OSVersion.VersionString;
378376
}
379377

380378
if (IsLinux())
381379
{
382-
var psi = new ProcessStartInfo
383-
{
384-
FileName = "uname",
385-
Arguments = "-a",
386-
RedirectStandardOutput = true
387-
};
380+
return _linuxDistroVersion ??= GetLinuxDistroVersion();
388381

389-
using (var uname = new ChildProcess(trace2, psi))
382+
string GetLinuxDistroVersion()
390383
{
391-
uname.Start(Trace2ProcessClass.Other);
392-
uname.Process.WaitForExit();
384+
// Let's first try to get the distribution information from /etc/os-release
385+
// (or /usr/lib/os-release) which is required in systemd distributions.
386+
// https://www.freedesktop.org/software/systemd/man/os-release.html
387+
foreach (string osReleasePath in new[] { "/etc/os-release", "/usr/lib/os-release" })
388+
{
389+
if (!File.Exists(osReleasePath))
390+
{
391+
continue;
392+
}
393+
394+
var props = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
395+
char[] split = { '=' };
396+
string[] lines = File.ReadAllLines(osReleasePath);
397+
foreach (string line in lines)
398+
{
399+
// Each line is a key="value" pair
400+
string[] kvp = line.Split(split, count: 2);
401+
if (kvp.Length != 2)
402+
{
403+
continue;
404+
}
405+
406+
props[kvp[0]] = kvp[1].Trim('"');
407+
}
408+
409+
// Try to get the PRETTY_NAME first which is a user-friendly description
410+
// including the distro name and version.
411+
if (props.TryGetValue("PRETTY_NAME", out string prettyName))
412+
{
413+
return prettyName;
414+
}
415+
416+
// Fall-back to (NAME || ID) + (VERSION || VERSION_ID || VERSION_CODENAME)?
417+
if (props.TryGetValue("NAME", out string distro) ||
418+
props.TryGetValue("ID", out distro))
419+
{
420+
if (props.TryGetValue("VERSION", out string version) ||
421+
props.TryGetValue("VERSION_ID", out version) ||
422+
props.TryGetValue("VERSION_CODENAME", out version))
423+
{
424+
return $"{distro} {version}";
425+
}
426+
427+
// Return just the distro name if we don't have a version
428+
return distro;
429+
}
430+
}
431+
432+
// If we couldn't get the distribution information from /etc/os-release
433+
// (for example if we're running on a non-systemd distribution), then let's
434+
// use `uname -a` to get at least some information.
435+
var psi = new ProcessStartInfo
436+
{
437+
FileName = "uname",
438+
Arguments = "-a",
439+
RedirectStandardOutput = true
440+
};
393441

394-
if (uname.ExitCode == 0)
442+
using (var uname = new ChildProcess(trace2, psi))
395443
{
396-
return uname.StandardOutput.ReadToEnd().Trim();
444+
uname.Start(Trace2ProcessClass.Other);
445+
uname.Process.WaitForExit();
446+
447+
if (uname.ExitCode == 0)
448+
{
449+
return uname.StandardOutput.ReadToEnd().Trim();
450+
}
397451
}
452+
453+
return "Unknown-Linux";
398454
}
399455
}
400456

0 commit comments

Comments
 (0)