Update reporting module (#5910)
* Updated reporting values. The reporter module can now emit the following extra values: - operating-system - installation-type - destination-type - next-scheduled-run When running from the built-in scheduler, it will automatically set `--next-scheduled-run` so it is applied to the reporting data. * Implemented an improved machine name reader. This fixes #5907 * Added a guard against sending too much of the destination url as the destination. * Fixed reading Windows Machine name * Removed extra Windows logic as it has no effect
This commit is contained in:
@@ -278,7 +278,7 @@ namespace Duplicati.Library.AutoUpdater
|
||||
/// <summary>
|
||||
/// The machine name
|
||||
/// </summary>
|
||||
public static string MachineName => System.Environment.MachineName;
|
||||
public static readonly string MachineName = MachineNameReader.GetMachineName();
|
||||
|
||||
/// <summary>
|
||||
/// The package type ID
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
// Copyright (C) 2025, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// Copyright (C) 2025, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
@@ -382,12 +382,13 @@ namespace Duplicati.Library.Main
|
||||
new CommandLineArgument("symlink-policy", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.SymlinkpolicyShort, Strings.Options.SymlinkpolicyLong("store", "ignore", "follow"), Enum.GetName(typeof(SymlinkStrategy), SymlinkStrategy.Store), null, Enum.GetNames(typeof(SymlinkStrategy))),
|
||||
new CommandLineArgument("hardlink-policy", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.HardlinkpolicyShort, Strings.Options.HardlinkpolicyLong("first", "all", "none"), Enum.GetName(typeof(HardlinkStrategy), HardlinkStrategy.All), null, Enum.GetNames(typeof(HardlinkStrategy))),
|
||||
new CommandLineArgument("exclude-files-attributes", CommandLineArgument.ArgumentType.String, Strings.Options.ExcludefilesattributesShort, Strings.Options.ExcludefilesattributesLong(Enum.GetNames(typeof(System.IO.FileAttributes)))),
|
||||
new CommandLineArgument("backup-name", CommandLineArgument.ArgumentType.String, Strings.Options.BackupnameShort, Strings.Options.BackupnameLong, DefaultBackupName),
|
||||
new CommandLineArgument("compression-extension-file", CommandLineArgument.ArgumentType.Path, Strings.Options.CompressionextensionfileShort, Strings.Options.CompressionextensionfileLong(DEFAULT_COMPRESSED_EXTENSION_FILE), DEFAULT_COMPRESSED_EXTENSION_FILE),
|
||||
|
||||
new CommandLineArgument("backup-id", CommandLineArgument.ArgumentType.String, Strings.Options.BackupidShort, Strings.Options.BackupidLong, ""),
|
||||
new CommandLineArgument("machine-id", CommandLineArgument.ArgumentType.String, Strings.Options.MachineidShort, Strings.Options.MachineidLong, Library.AutoUpdater.UpdaterManager.InstallID),
|
||||
new CommandLineArgument("machine-name", CommandLineArgument.ArgumentType.String, Strings.Options.MachinenameShort, Strings.Options.MachinenameLong, Library.AutoUpdater.UpdaterManager.MachineName),
|
||||
new CommandLineArgument("backup-id", CommandLineArgument.ArgumentType.String, Strings.Options.BackupidShort, Strings.Options.BackupidLong, ""),
|
||||
new CommandLineArgument("backup-name", CommandLineArgument.ArgumentType.String, Strings.Options.BackupnameShort, Strings.Options.BackupnameLong, DefaultBackupName),
|
||||
new CommandLineArgument("next-scheduled-run", CommandLineArgument.ArgumentType.String, Strings.Options.NextscheduledrunShort, Strings.Options.NextscheduledrunLong),
|
||||
|
||||
new CommandLineArgument("verbose", CommandLineArgument.ArgumentType.Boolean, Strings.Options.VerboseShort, Strings.Options.VerboseLong, "false", null, null, Strings.Options.VerboseDeprecated),
|
||||
new CommandLineArgument("full-result", CommandLineArgument.ArgumentType.Boolean, Strings.Options.FullresultShort, Strings.Options.FullresultLong, "false"),
|
||||
|
||||
@@ -171,6 +171,8 @@ namespace Duplicati.Library.Main.Strings
|
||||
public static string MachineidShort { get { return LC.L(@"Machine ID"); } }
|
||||
public static string MachinenameLong { get { return LC.L(@"The name of the machine running the backup. This can be used to identify the machine when sending mail or running scripts."); } }
|
||||
public static string MachinenameShort { get { return LC.L(@"Machine name"); } }
|
||||
public static string NextscheduledrunShort { get { return LC.L(@"The time of the next scheduled run"); } }
|
||||
public static string NextscheduledrunLong { get { return LC.L(@"This property is a reporting option and does not affect the actual scheduled time. Use this option to inform a reporting destination about the next expected time the backup will run."); } }
|
||||
public static string CompressionextensionfileLong(string path) { return LC.L(@"Use this option to point to a text file where each line contains a file extension that indicates a non-compressible file. Files that have an extension found in the file will not be compressed, but simply stored in the archive. The file format ignores any lines that do not start with a period, and considers a space to indicate the end of the extension. A default file is supplied, that also serves as an example. The default file is placed in {0}.", path); }
|
||||
public static string CompressionextensionfileShort { get { return LC.L(@"Manage non-compressible file extensions"); } }
|
||||
public static string BlocksizeLong { get { return LC.L(@"The block size determines how files are fragmented. Choosing a large value will cause a larger overhead on file changes, choosing a small value will cause a large overhead on storage of file lists. Note that the value cannot be changed after remote files are created."); } }
|
||||
|
||||
@@ -315,6 +315,108 @@ namespace Duplicati.Library.Modules.Builtin
|
||||
protected virtual string ReplaceTemplate(string input, object result, Exception exception, bool subjectline, ResultExportFormat format)
|
||||
=> ReplaceTemplate(input, result, exception, subjectline, ResultFormatSerializerProvider.GetSerializer(format));
|
||||
|
||||
/// <summary>
|
||||
/// The operation name template key
|
||||
/// </summary>
|
||||
private const string OPERATIONNAME = "OperationName";
|
||||
/// <summary>
|
||||
/// The remote url template key
|
||||
/// </summary>
|
||||
private const string REMOTEURL = "RemoteUrl";
|
||||
/// <summary>
|
||||
/// The local path template key
|
||||
/// </summary>
|
||||
private const string LOCALPATH = "LocalPath";
|
||||
/// <summary>
|
||||
/// The parsed result template key
|
||||
/// </summary>
|
||||
private const string PARSEDRESULT = "ParsedResult";
|
||||
/// <summary>
|
||||
/// The machine id template key
|
||||
/// </summary>
|
||||
private const string MACHINE_ID = "machine-id";
|
||||
/// <summary>
|
||||
/// The backup id template key
|
||||
/// </summary>
|
||||
private const string BACKUP_ID = "backup-id";
|
||||
/// <summary>
|
||||
/// The backup name template key
|
||||
/// </summary>
|
||||
private const string BACKUP_NAME = "backup-name";
|
||||
/// <summary>
|
||||
/// The machine name template key
|
||||
/// </summary>
|
||||
private const string MACHINE_NAME = "machine-name";
|
||||
/// <summary>
|
||||
/// The operating system template key
|
||||
/// </summary>
|
||||
private const string OPERATING_SYSTEM = "operating-system";
|
||||
/// <summary>
|
||||
/// The installation type template key
|
||||
/// </summary>
|
||||
private const string INSTALLATION_TYPE = "installation-type";
|
||||
/// <summary>
|
||||
/// The destination type template key
|
||||
/// </summary>
|
||||
private const string DESTINATION_TYPE = "destination-type";
|
||||
/// <summary>
|
||||
/// The next scheduled run template key
|
||||
/// </summary>
|
||||
private const string NEXT_SCHEDULED_RUN = "next-scheduled-run";
|
||||
|
||||
/// <summary>
|
||||
/// The list of regular template keys
|
||||
/// </summary>
|
||||
private static readonly IReadOnlySet<string> OPERATION_TEMPLATE_KEYS = new HashSet<string>([
|
||||
OPERATIONNAME, REMOTEURL, LOCALPATH, PARSEDRESULT
|
||||
], StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// The list of extra template keys
|
||||
/// </summary>
|
||||
private static readonly IReadOnlySet<string> EXTRA_TEMPLATE_KEYS = new HashSet<string>([
|
||||
MACHINE_ID, BACKUP_ID, BACKUP_NAME, MACHINE_NAME,
|
||||
OPERATING_SYSTEM, INSTALLATION_TYPE, DESTINATION_TYPE, NEXT_SCHEDULED_RUN
|
||||
], StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default value for a template key
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the template key</param>
|
||||
/// <returns>The default value</returns>
|
||||
private string GetDefaultValue(string name)
|
||||
{
|
||||
switch (name)
|
||||
{
|
||||
case OPERATIONNAME:
|
||||
return m_operationname;
|
||||
case REMOTEURL:
|
||||
return m_remoteurl;
|
||||
case LOCALPATH:
|
||||
return m_localpath == null ? "" : string.Join(System.IO.Path.PathSeparator.ToString(), m_localpath);
|
||||
case PARSEDRESULT:
|
||||
return m_parsedresultlevel;
|
||||
case MACHINE_ID:
|
||||
return AutoUpdater.UpdaterManager.MachineID;
|
||||
case BACKUP_ID:
|
||||
return Utility.Utility.ByteArrayAsHexString(Utility.Utility.RepeatedHashWithSalt(m_remoteurl, SALT));
|
||||
case BACKUP_NAME:
|
||||
return System.IO.Path.GetFileNameWithoutExtension(Utility.Utility.getEntryAssembly().Location);
|
||||
case MACHINE_NAME:
|
||||
return AutoUpdater.UpdaterManager.MachineName;
|
||||
case OPERATING_SYSTEM:
|
||||
return OperatingSystem.IsWindows() ? "Windows" : OperatingSystem.IsLinux() ? "Linux" : OperatingSystem.IsMacOS() ? "MacOS" : "Unknown";
|
||||
case INSTALLATION_TYPE:
|
||||
return AutoUpdater.UpdaterManager.PackageTypeId;
|
||||
case DESTINATION_TYPE:
|
||||
// Only return the url scheme, as the rest could contain sensitive information
|
||||
var ix = m_remoteurl?.IndexOf("://", StringComparison.OrdinalIgnoreCase) ?? -1;
|
||||
return ix >= 0 && ix < 15 ? m_remoteurl.Substring(0, ix) : "file";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to perform template expansion
|
||||
/// </summary>
|
||||
@@ -331,55 +433,29 @@ namespace Duplicati.Library.Modules.Builtin
|
||||
{
|
||||
var extra = new Dictionary<string, string>();
|
||||
|
||||
if (input.IndexOf("%OPERATIONNAME%", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
extra["OperationName"] = m_operationname;
|
||||
if (input.IndexOf("%REMOTEURL%", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
extra["RemoteUrl"] = m_remoteurl;
|
||||
if (input.IndexOf("%LOCALPATH%", StringComparison.OrdinalIgnoreCase) >= 0 && m_localpath != null)
|
||||
extra["LocalPath"] = string.Join(System.IO.Path.PathSeparator.ToString(), m_localpath);
|
||||
if (input.IndexOf("%PARSEDRESULT%", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
extra["ParsedResult"] = m_parsedresultlevel;
|
||||
|
||||
// If the options contains the key, it is captured by the loop over m_options
|
||||
// so we only patch it in case it is missing
|
||||
|
||||
if (input.IndexOf("%machine-id%", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
if (!m_options.ContainsKey("machine-id"))
|
||||
extra["machine-id"] = Library.AutoUpdater.UpdaterManager.MachineID;
|
||||
}
|
||||
|
||||
if (input.IndexOf("%backup-id%", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
if (!m_options.ContainsKey("backup-id"))
|
||||
extra["backup-id"] = Library.Utility.Utility.ByteArrayAsHexString(Library.Utility.Utility.RepeatedHashWithSalt(m_remoteurl, SALT));
|
||||
}
|
||||
|
||||
if (input.IndexOf("%backup-name%", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
if (!m_options.ContainsKey("backup-name"))
|
||||
extra["backup-name"] = System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.getEntryAssembly().Location);
|
||||
}
|
||||
|
||||
if (input.IndexOf("%machine-name%", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
if (!m_options.ContainsKey("machine-name"))
|
||||
extra["machine-name"] = Library.AutoUpdater.UpdaterManager.MachineName;
|
||||
}
|
||||
// Add the default values, if found in the template
|
||||
foreach (var key in OPERATION_TEMPLATE_KEYS)
|
||||
if (input.IndexOf($"%{key}%", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
extra[key] = GetDefaultValue(key);
|
||||
|
||||
// Add any options that are whitelisted or used in the template
|
||||
foreach (KeyValuePair<string, string> kv in m_options)
|
||||
if (input.IndexOf($"%{kv.Key}%", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
if (EXTRA_TEMPLATE_KEYS.Contains(kv.Key) || input.IndexOf($"%{kv.Key}%", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
extra[kv.Key] = kv.Value;
|
||||
|
||||
// Add any missing default values
|
||||
foreach (var key in EXTRA_TEMPLATE_KEYS)
|
||||
if (!extra.ContainsKey(key))
|
||||
extra[key] = GetDefaultValue(key);
|
||||
|
||||
return resultFormatSerializer.Serialize(result, exception, LogLines, extra);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
input = Regex.Replace(input, "\\%OPERATIONNAME\\%", m_operationname ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
input = Regex.Replace(input, "\\%REMOTEURL\\%", m_remoteurl ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
input = Regex.Replace(input, "\\%LOCALPATH\\%", m_localpath == null ? "" : string.Join(System.IO.Path.PathSeparator.ToString(), m_localpath), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
input = Regex.Replace(input, "\\%PARSEDRESULT\\%", m_parsedresultlevel ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
foreach (var key in OPERATION_TEMPLATE_KEYS)
|
||||
input = Regex.Replace(input, $"%{key}%", GetDefaultValue(key) ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
if (subjectline)
|
||||
{
|
||||
input = Regex.Replace(input, "\\%RESULT\\%", m_parsedresultlevel ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
@@ -393,18 +469,11 @@ namespace Duplicati.Library.Modules.Builtin
|
||||
foreach (KeyValuePair<string, string> kv in m_options)
|
||||
input = Regex.Replace(input, "\\%" + kv.Key + "\\%", kv.Value ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
if (!m_options.ContainsKey("backup-name"))
|
||||
input = Regex.Replace(input, "\\%backup-name\\%", System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.getEntryAssembly().Location) ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
if (!m_options.ContainsKey("machine-name"))
|
||||
input = Regex.Replace(input, "\\%machine-name\\%", Library.AutoUpdater.UpdaterManager.MachineName, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
if (!m_options.ContainsKey("backup-id"))
|
||||
input = Regex.Replace(input, "\\%backup-id\\%", Library.Utility.Utility.ByteArrayAsHexString(Library.Utility.Utility.RepeatedHashWithSalt(m_remoteurl, SALT)), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
if (!m_options.ContainsKey("machine-id"))
|
||||
input = Regex.Replace(input, "\\%machine-id\\%", Library.AutoUpdater.UpdaterManager.MachineID, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
foreach (var key in EXTRA_TEMPLATE_KEYS)
|
||||
if (!m_options.ContainsKey(key))
|
||||
input = Regex.Replace(input, $"%{key}%", GetDefaultValue(key) ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
// Remove any remaining template keys
|
||||
input = Regex.Replace(input, "\\%[^\\%]+\\%", "");
|
||||
return input;
|
||||
}
|
||||
|
||||
@@ -357,8 +357,23 @@ namespace Duplicati.Server
|
||||
}
|
||||
else
|
||||
{
|
||||
Dictionary<string, string> taskOptions = null;
|
||||
try
|
||||
{
|
||||
var nextRun = GetNextValidTime(start,
|
||||
new DateTime(
|
||||
Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks),
|
||||
DateTimeKind.Utc), sc.Repeat, sc.AllowedDays, timeZoneInfo);
|
||||
|
||||
taskOptions = new Dictionary<string, string>()
|
||||
{ { "next-scheduled-run", Utility.SerializeDateTime(nextRun.ToUniversalTime()) } };
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
jobsToRun.Add(Server.Runner.CreateTask(
|
||||
Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry));
|
||||
Serialization.DuplicatiOperation.Backup, entry, taskOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Management;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Duplicati.Library.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// Class for reading the machine name
|
||||
/// </summary>
|
||||
public static class MachineNameReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes a best effort to get the machine name
|
||||
/// </summary>
|
||||
/// <returns>The machine name</returns>
|
||||
public static string GetMachineName()
|
||||
{
|
||||
string? machineName = null;
|
||||
if (OperatingSystem.IsWindows())
|
||||
machineName = GetMachineNameWindows();
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
machineName = GetMachineNameMacOS();
|
||||
else if (OperatingSystem.IsLinux())
|
||||
machineName = GetMachineNameLinux();
|
||||
|
||||
return string.IsNullOrWhiteSpace(machineName)
|
||||
? Environment.MachineName
|
||||
: machineName;
|
||||
}
|
||||
/// <summary>
|
||||
/// Executes a command and reads the output
|
||||
/// </summary>
|
||||
/// <param name="command">The command to execute</param>
|
||||
/// <param name="arguments">The arguments to pass to the command</param>
|
||||
/// <returns>The output of the command</returns>
|
||||
private static string ExecuteAndReadOutput(string command, string arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = command,
|
||||
Arguments = arguments,
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
process.WaitForExit(TimeSpan.FromSeconds(1));
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill();
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
return string.Empty;
|
||||
|
||||
return process.StandardOutput.ReadToEnd().Trim();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the machine name if running MacOS
|
||||
/// </summary>
|
||||
/// <returns>The machine name</returns>
|
||||
[SupportedOSPlatform("macos")]
|
||||
private static string? GetMachineNameMacOS()
|
||||
=> ExecuteAndReadOutput("scutil", "--get ComputerName");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the machine name if running Windows
|
||||
/// </summary>
|
||||
/// <returns>The machine name</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static string? GetMachineNameWindows()
|
||||
=> null; // No special handling for Windows, always uses NetBIOS name
|
||||
|
||||
/// <summary>
|
||||
/// Gets the machine name if running Linux
|
||||
/// </summary>
|
||||
/// <returns>The machine name</returns>
|
||||
[SupportedOSPlatform("linux")]
|
||||
private static string? GetMachineNameLinux()
|
||||
=> null; // No special handling for Linux, always uses hostname
|
||||
}
|
||||
Reference in New Issue
Block a user