diff --git a/README.md b/README.md index 504b5cb..5ae2e2b 100644 --- a/README.md +++ b/README.md @@ -71,14 +71,14 @@ dotnet run --project src/Sms.TaskTwo.Avalonia/Sms.TaskTwo.Avalonia.csproj ### Windows -- Чтение/запись: `EnvironmentVariableTarget.User` (реестр `HKEY_CURRENT_USER\Environment`). -- После записи отправляется `WM_SETTINGCHANGE`, чтобы обновить env в уже запущенных GUI-приложениях. +- Чтение/запись: реестр `HKEY_CURRENT_USER\Environment`. +- **Применить к сессии** (`ReloadEnvironment`): обновляет env текущего процесса и рассылает `WM_SETTINGCHANGE` для других GUI-приложений. ### Linux -- Запись в `~/.config/environment.d/99-sms-task-two.conf` (формат systemd `KEY=value`, значения с пробелами в кавычках). -- Чтение: merge всех `*.conf` в `environment.d`, затем fallback на env текущего процесса. -- Для новых login-сессий может потребоваться перелогин или `systemctl --user import-environment` — ограничение systemd, не ошибка приложения. +- Запись в `~/.config/environment.d/` (systemd `KEY=value`). +- **Применить к сессии** (`ReloadEnvironment`): текущий процесс, `systemctl --user set-environment`, при наличии — `dbus-update-environment`. +- Новые login-сессии могут потребовать перелогин — ограничение systemd. ## Предположения (ТЗ) diff --git a/src/Sms.Environment.Linux/LinuxEnvironmentVariableStore.cs b/src/Sms.Environment.Linux/LinuxEnvironmentVariableStore.cs index bf32c24..cbd4fd2 100644 --- a/src/Sms.Environment.Linux/LinuxEnvironmentVariableStore.cs +++ b/src/Sms.Environment.Linux/LinuxEnvironmentVariableStore.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Sms.Environment; namespace Sms.Environment.Linux; @@ -5,6 +6,7 @@ namespace Sms.Environment.Linux; public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore { private const string ManagedFileName = "99-sms-task-two.conf"; + private const int SystemctlBatchSize = 32; private readonly string _managedFilePath; private readonly string _environmentDirectory; @@ -119,6 +121,7 @@ public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore } System.Environment.SetEnvironmentVariable(name, null); + TryRunSystemctl(["--user", "unset-environment", name], out _); } catch (Exception ex) when (ex is UnauthorizedAccessException or IOException) { @@ -128,6 +131,142 @@ public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore } } + public EnvironmentReloadResult ReloadEnvironment() + { + var variables = GetUserPersistedEnvironment(); + ApplyToCurrentProcess(variables); + + if (variables.Count == 0) + { + return new EnvironmentReloadResult + { + Success = true, + Message = "Пользовательских переменных для применения нет.", + }; + } + + var messages = new List(); + if (TryApplyViaSystemctl(variables, out var systemctlError)) + { + messages.Add("systemd user manager обновлён (systemctl --user set-environment)."); + } + else if (systemctlError is not null) + { + messages.Add($"systemctl: {systemctlError}"); + } + + if (TryApplyViaDbusUpdateEnvironment(variables, out var dbusError)) + { + messages.Add("D-Bus session обновлён (dbus-update-environment)."); + } + else if (dbusError is not null) + { + messages.Add($"dbus-update-environment: {dbusError}"); + } + + var processApplied = messages.Count > 0 || variables.Count > 0; + return new EnvironmentReloadResult + { + Success = processApplied, + Message = processApplied + ? $"Применено {variables.Count} переменных к текущему процессу. {string.Join(' ', messages)}" + : "Не удалось применить переменные к сессии.", + }; + } + + private static void ApplyToCurrentProcess(IReadOnlyDictionary variables) + { + foreach (var pair in variables) + { + System.Environment.SetEnvironmentVariable(pair.Key, pair.Value); + } + } + + private static bool TryApplyViaSystemctl( + IReadOnlyDictionary variables, + out string? errorMessage) + { + errorMessage = null; + var pairs = variables.ToList(); + for (var offset = 0; offset < pairs.Count; offset += SystemctlBatchSize) + { + var arguments = new List { "--user", "set-environment" }; + foreach (var pair in pairs.Skip(offset).Take(SystemctlBatchSize)) + { + arguments.Add($"{pair.Key}={pair.Value}"); + } + + if (!TryRunSystemctl(arguments, out errorMessage)) + { + return false; + } + } + + return true; + } + + private static bool TryApplyViaDbusUpdateEnvironment( + IReadOnlyDictionary variables, + out string? errorMessage) + { + var arguments = new List(); + foreach (var pair in variables) + { + arguments.Add($"{pair.Key}={pair.Value}"); + } + + return TryRunCommand("dbus-update-environment", arguments, out errorMessage); + } + + private static bool TryRunSystemctl(IReadOnlyList arguments, out string? errorMessage) => + TryRunCommand("systemctl", arguments, out errorMessage); + + private static bool TryRunCommand( + string fileName, + IReadOnlyList arguments, + out string? errorMessage) + { + errorMessage = null; + try + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + }; + foreach (var argument in arguments) + { + startInfo.ArgumentList.Add(argument); + } + + using var process = Process.Start(startInfo); + if (process is null) + { + errorMessage = "не удалось запустить процесс"; + return false; + } + + var stderr = process.StandardError.ReadToEnd(); + process.WaitForExit(5000); + if (process.ExitCode == 0) + { + return true; + } + + errorMessage = string.IsNullOrWhiteSpace(stderr) + ? $"код выхода {process.ExitCode}" + : stderr.Trim(); + return false; + } + catch (Exception ex) + { + errorMessage = ex.Message; + return false; + } + } + private Dictionary LoadManagedFile() { if (!File.Exists(_managedFilePath)) diff --git a/src/Sms.Environment.Windows/WindowsEnvironmentVariableStore.cs b/src/Sms.Environment.Windows/WindowsEnvironmentVariableStore.cs index 12cfdb0..76e3c0a 100644 --- a/src/Sms.Environment.Windows/WindowsEnvironmentVariableStore.cs +++ b/src/Sms.Environment.Windows/WindowsEnvironmentVariableStore.cs @@ -83,6 +83,25 @@ public sealed class WindowsEnvironmentVariableStore : IEnvironmentVariableStore BroadcastEnvironmentChange(); } + public EnvironmentReloadResult ReloadEnvironment() + { + var variables = GetUserPersistedEnvironment(); + foreach (var pair in variables) + { + System.Environment.SetEnvironmentVariable(pair.Key, pair.Value, EnvironmentVariableTarget.Process); + } + + BroadcastEnvironmentChange(); + + return new EnvironmentReloadResult + { + Success = true, + Message = variables.Count == 0 + ? "Пользовательских переменных для применения нет." + : $"Применено {variables.Count} переменных к текущему процессу и отправлено WM_SETTINGCHANGE для других приложений.", + }; + } + private static Dictionary ToDictionary(System.Collections.IDictionary source) { var result = new Dictionary(StringComparer.Ordinal); diff --git a/src/Sms.Environment/EnvironmentReloadResult.cs b/src/Sms.Environment/EnvironmentReloadResult.cs new file mode 100644 index 0000000..bc958d3 --- /dev/null +++ b/src/Sms.Environment/EnvironmentReloadResult.cs @@ -0,0 +1,8 @@ +namespace Sms.Environment; + +public sealed class EnvironmentReloadResult +{ + public required bool Success { get; init; } + + public required string Message { get; init; } +} diff --git a/src/Sms.Environment/IEnvironmentVariableStore.cs b/src/Sms.Environment/IEnvironmentVariableStore.cs index 8a9f250..109aa9a 100644 --- a/src/Sms.Environment/IEnvironmentVariableStore.cs +++ b/src/Sms.Environment/IEnvironmentVariableStore.cs @@ -19,4 +19,6 @@ public interface IEnvironmentVariableStore bool IsPersistedInUserStore(string name); void RemoveFromUserStore(string name); + + EnvironmentReloadResult ReloadEnvironment(); } diff --git a/src/Sms.TaskTwo.Avalonia/Views/MainWindow.axaml b/src/Sms.TaskTwo.Avalonia/Views/MainWindow.axaml index 9b693ef..39ebab8 100644 --- a/src/Sms.TaskTwo.Avalonia/Views/MainWindow.axaml +++ b/src/Sms.TaskTwo.Avalonia/Views/MainWindow.axaml @@ -43,7 +43,7 @@ +