Применение к сессии, USE заменён на checkbox

This commit is contained in:
2026-06-04 19:55:29 +03:00
parent caac16f88b
commit b6eb8026f0
10 changed files with 282 additions and 78 deletions

View File

@@ -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.
## Предположения (ТЗ)

View File

@@ -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<string>();
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<string, string> variables)
{
foreach (var pair in variables)
{
System.Environment.SetEnvironmentVariable(pair.Key, pair.Value);
}
}
private static bool TryApplyViaSystemctl(
IReadOnlyDictionary<string, string> variables,
out string? errorMessage)
{
errorMessage = null;
var pairs = variables.ToList();
for (var offset = 0; offset < pairs.Count; offset += SystemctlBatchSize)
{
var arguments = new List<string> { "--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<string, string> variables,
out string? errorMessage)
{
var arguments = new List<string>();
foreach (var pair in variables)
{
arguments.Add($"{pair.Key}={pair.Value}");
}
return TryRunCommand("dbus-update-environment", arguments, out errorMessage);
}
private static bool TryRunSystemctl(IReadOnlyList<string> arguments, out string? errorMessage) =>
TryRunCommand("systemctl", arguments, out errorMessage);
private static bool TryRunCommand(
string fileName,
IReadOnlyList<string> 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<string, string> LoadManagedFile()
{
if (!File.Exists(_managedFilePath))

View File

@@ -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<string, string> ToDictionary(System.Collections.IDictionary source)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);

View File

@@ -0,0 +1,8 @@
namespace Sms.Environment;
public sealed class EnvironmentReloadResult
{
public required bool Success { get; init; }
public required string Message { get; init; }
}

View File

@@ -19,4 +19,6 @@ public interface IEnvironmentVariableStore
bool IsPersistedInUserStore(string name);
void RemoveFromUserStore(string name);
EnvironmentReloadResult ReloadEnvironment();
}

View File

@@ -43,7 +43,7 @@
<Grid Grid.Row="1"
Margin="12,8,12,0"
ColumnDefinitions="*,Auto"
ColumnDefinitions="*,Auto,Auto"
ColumnSpacing="8">
<CheckBox Content="Отображать все переменные"
IsChecked="{Binding ShowAllVariables}"
@@ -52,6 +52,10 @@
Content="Обновить"
Command="{Binding RefreshCommand}"
MinWidth="100" />
<Button Grid.Column="2"
Content="Применить к сессии"
Command="{Binding ReloadEnvironmentCommand}"
MinWidth="140" />
</Grid>
<Grid Grid.Row="2"
@@ -73,11 +77,17 @@
MinWidth="100" />
</Grid>
<TextBlock Grid.Row="3"
Margin="12,4,12,0"
Foreground="#C62828"
Text="{Binding AddVariableError}"
IsVisible="{Binding AddVariableError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<StackPanel Grid.Row="3"
Margin="12,4,12,0"
Spacing="2">
<TextBlock Foreground="#C62828"
Text="{Binding AddVariableError}"
IsVisible="{Binding AddVariableError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<TextBlock Foreground="#2E7D32"
Text="{Binding ReloadEnvironmentMessage}"
TextWrapping="Wrap"
IsVisible="{Binding ReloadEnvironmentMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
</StackPanel>
<DataGrid x:Name="VariablesGrid"
Grid.Row="4"
@@ -93,10 +103,17 @@
BorderBrush="#B0B0B0"
LoadingRow="OnLoadingRow">
<DataGrid.Columns>
<DataGridTextColumn Header=""
Binding="{Binding UserStoreBadge}"
IsReadOnly="True"
Width="56" />
<DataGridTemplateColumn Header=""
Width="44">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:EnvironmentVariableRowViewModel">
<CheckBox IsChecked="{Binding UseUserStore, Mode=TwoWay}"
IsEnabled="{Binding CanChangeUserStore}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Поле"
Binding="{Binding Field}"
IsReadOnly="True"
@@ -115,17 +132,6 @@
Binding="{Binding Comment, Mode=TwoWay}"
Width="240"
MinWidth="180" />
<DataGridTemplateColumn Header=""
Width="80">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:EnvironmentVariableRowViewModel">
<Button Content="Удалить"
Command="{Binding DeleteFromUserStoreCommand}"
Padding="6,2"
FontSize="11" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>

View File

@@ -87,7 +87,7 @@ public partial class MainWindow : Window
gridRow.Classes.Add("custom");
}
if (row.IsPersistedInUserStore)
if (row.UseUserStore)
{
gridRow.Classes.Add("userStore");
}

View File

@@ -145,6 +145,14 @@ public sealed class EnvironmentVariablesService
public string? GetProcessValue(string name) =>
_store.GetProcessEnvironment().TryGetValue(name, out var value) ? value : null;
public EnvironmentReloadResult ReloadEnvironment()
{
var result = _store.ReloadEnvironment();
_log.WriteLine(
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Reload environment: success={result.Success}; {result.Message}");
return result;
}
public void SaveValue(string name, string value)
{
var previous = _store.GetUserPersistedValue(name);

View File

@@ -1,5 +1,4 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Sms.TaskTwo.Core.Models;
using Sms.TaskTwo.Core.Services;
@@ -8,14 +7,19 @@ namespace Sms.TaskTwo.ViewModels;
public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
{
private readonly EnvironmentVariablesService _service;
private readonly Action? _onCustomRemoved;
private bool _isLoading;
public EnvironmentVariableRowViewModel(EnvironmentVariableRow row, EnvironmentVariablesService service)
public EnvironmentVariableRowViewModel(
EnvironmentVariableRow row,
EnvironmentVariablesService service,
Action? onCustomRemoved = null)
{
Field = row.Field;
IsFromAppSettings = row.IsFromAppSettings;
IsCustom = row.IsCustom;
_service = service;
_onCustomRemoved = onCustomRemoved;
ApplySnapshot(row, suppressSave: true);
RefreshActualValue(_service.GetProcessValue(Field));
}
@@ -26,8 +30,10 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
public bool IsCustom { get; }
public bool CanChangeUserStore => !IsFromAppSettings;
[ObservableProperty]
private bool _isPersistedInUserStore;
private bool _useUserStore;
[ObservableProperty]
private string _actualValue = string.Empty;
@@ -35,36 +41,60 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
public string ActualValueDisplay =>
string.IsNullOrEmpty(ActualValue) ? "<нет>" : ActualValue;
public string UserStoreBadge => IsPersistedInUserStore ? "USER" : string.Empty;
[ObservableProperty]
private string _requiredValue = string.Empty;
[ObservableProperty]
private string _comment = string.Empty;
partial void OnIsPersistedInUserStoreChanged(bool value)
partial void OnUseUserStoreChanged(bool value)
{
OnPropertyChanged(nameof(UserStoreBadge));
DeleteFromUserStoreCommand.NotifyCanExecuteChanged();
RowAppearanceChanged?.Invoke(this, EventArgs.Empty);
if (_isLoading)
{
return;
}
if (IsFromAppSettings && !value)
{
BeginLoad();
UseUserStore = true;
EndLoad();
return;
}
if (value)
{
_service.SaveValue(Field, RequiredValue);
RefreshActualValue(_service.GetProcessValue(Field));
return;
}
if (IsCustom)
{
_service.RemoveVariable(Field);
_onCustomRemoved?.Invoke();
return;
}
_service.DeleteFromUserStore(Field);
BeginLoad();
RequiredValue = _service.GetDisplayValue(Field);
EndLoad();
RefreshActualValue(_service.GetProcessValue(Field));
}
partial void OnActualValueChanged(string value) => OnPropertyChanged(nameof(ActualValueDisplay));
partial void OnRequiredValueChanged(string value)
{
if (_isLoading)
if (_isLoading || !UseUserStore)
{
return;
}
_service.SaveValue(Field, value);
if (!IsPersistedInUserStore)
{
IsPersistedInUserStore = true;
}
RefreshActualValue(_service.GetProcessValue(Field));
}
@@ -79,7 +109,7 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
RequiredValue = row.Value;
Comment = row.Comment;
IsPersistedInUserStore = row.IsPersistedInUserStore;
UseUserStore = row.IsFromAppSettings || row.IsPersistedInUserStore;
if (suppressSave)
{
@@ -105,26 +135,4 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
_service.SaveComment(Field, value);
}
[RelayCommand(CanExecute = nameof(CanDeleteFromUserStore))]
private void DeleteFromUserStore()
{
if (IsCustom)
{
_service.RemoveVariable(Field);
Removed?.Invoke(this, EventArgs.Empty);
return;
}
_service.DeleteFromUserStore(Field);
BeginLoad();
IsPersistedInUserStore = false;
RequiredValue = _service.GetDisplayValue(Field);
EndLoad();
RefreshActualValue(_service.GetProcessValue(Field));
}
public event EventHandler? Removed;
private bool CanDeleteFromUserStore() => IsPersistedInUserStore || IsCustom;
}

View File

@@ -1,6 +1,7 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Sms.TaskTwo.Core.Models;
using Sms.TaskTwo.Core.Services;
namespace Sms.TaskTwo.ViewModels;
@@ -31,6 +32,9 @@ public sealed partial class MainWindowViewModel : ObservableObject
[ObservableProperty]
private string? _addVariableError;
[ObservableProperty]
private string? _reloadEnvironmentMessage;
partial void OnShowAllVariablesChanged(bool value) => SyncRowsFromService();
partial void OnNewVariableNameChanged(string value) => AddVariableCommand.NotifyCanExecuteChanged();
@@ -42,6 +46,14 @@ public sealed partial class MainWindowViewModel : ObservableObject
RefreshProcessStates();
}
[RelayCommand]
private void ReloadEnvironment()
{
var result = _service.ReloadEnvironment();
ReloadEnvironmentMessage = result.Message;
RefreshProcessStates();
}
[RelayCommand(CanExecute = nameof(CanAddVariable))]
private void AddVariable()
{
@@ -57,18 +69,23 @@ public sealed partial class MainWindowViewModel : ObservableObject
NewVariableValue = string.Empty;
var row = _service.GetRowSnapshot(name);
var viewModel = new EnvironmentVariableRowViewModel(row, _service);
viewModel.Removed += OnRowRemoved;
Rows.Insert(FindInsertIndexForCustomVariable(), viewModel);
Rows.Insert(FindInsertIndexForCustomVariable(), CreateRowViewModel(row));
}
private void OnRowRemoved(object? sender, EventArgs e)
private EnvironmentVariableRowViewModel CreateRowViewModel(EnvironmentVariableRow row)
{
if (sender is EnvironmentVariableRowViewModel row)
{
row.Removed -= OnRowRemoved;
Rows.Remove(row);
}
EnvironmentVariableRowViewModel? viewModel = null;
viewModel = new EnvironmentVariableRowViewModel(
row,
_service,
onCustomRemoved: () =>
{
if (viewModel is not null)
{
Rows.Remove(viewModel);
}
});
return viewModel;
}
private int FindInsertIndexForCustomVariable()
@@ -93,10 +110,8 @@ public sealed partial class MainWindowViewModel : ObservableObject
for (var index = Rows.Count - 1; index >= 0; index--)
{
var row = Rows[index];
if (data.All(item => item.Field != row.Field))
if (data.All(item => item.Field != Rows[index].Field))
{
row.Removed -= OnRowRemoved;
Rows.RemoveAt(index);
}
}
@@ -113,8 +128,7 @@ public sealed partial class MainWindowViewModel : ObservableObject
}
else
{
viewModel = new EnvironmentVariableRowViewModel(row, _service);
viewModel.Removed += OnRowRemoved;
viewModel = CreateRowViewModel(row);
existingByField[row.Field] = viewModel;
}