Compare commits
6
Commits
1c18f4893d
...
f460e87253
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f460e87253 | ||
|
|
433dc9a50d | ||
|
|
45b1379853 | ||
|
|
97fbc1d746 | ||
|
|
0213b229a9 | ||
|
|
876e4f5d50 |
@@ -21,7 +21,8 @@ namespace Launcher.Next.App;
|
||||
/// </summary>
|
||||
internal sealed record LocalParityLaunchProfile(
|
||||
int SchemaVersion, string Name, string Root, string SettingsPath, string DownloadListPath,
|
||||
string OverlayPath, string CatalogRoot, string PatternName, string EvidenceRoot);
|
||||
string OverlayPath, string CatalogRoot, string PatternName, string EvidenceRoot, string? NetworkSettingsEndpoint = null,
|
||||
LegacySettingsSnapshot? SettingsSnapshot = null);
|
||||
|
||||
internal sealed record LocalParityPrepared(LocalParityLaunchProfile Profile, PackageRequirement Requirement,
|
||||
PackageManifest Manifest, ImmutableLocalPackage Package, PackageActivationResult Activation,
|
||||
@@ -41,7 +42,13 @@ internal sealed class LocalParityLaunchService
|
||||
if (profile.SchemaVersion != 1 || string.IsNullOrWhiteSpace(profile.Name) || string.IsNullOrWhiteSpace(profile.PatternName))
|
||||
throw new InvalidOperationException("LOCAL_PARITY_PROFILE_INVALID");
|
||||
ValidateProfile(profile);
|
||||
return profile with { Root = Path.GetFullPath(profile.Root) };
|
||||
profile = profile with { Root = Path.GetFullPath(profile.Root) };
|
||||
if (!string.IsNullOrWhiteSpace(profile.NetworkSettingsEndpoint))
|
||||
{
|
||||
var snapshot = LegacyNetworkConfiguration.AcquireApprovedSnapshot(profile.NetworkSettingsEndpoint, profile.Root);
|
||||
profile = profile with { SettingsPath = snapshot.LocalSnapshotPath, SettingsSnapshot = snapshot };
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
public async Task<LocalParityPrepared> PrepareAsync(LocalParityLaunchProfile profile, CancellationToken ct)
|
||||
@@ -99,7 +106,8 @@ internal sealed class LocalParityLaunchService
|
||||
}
|
||||
var loaded = runtimeConfirmed;
|
||||
var terminal = alive && loaded ? "Succeeded" : "Blocked";
|
||||
var evidence = new { schemaVersion = 2, launcherCommit = ThisCommit(), autodeskCommit = "68f5006", operationId, configuration = prepared.Profile.Name, selectedPattern = prepared.Profile.PatternName, orderedPluginIds = new[] { prepared.Requirement.PackageId }, exactPackage = new { prepared.Package.PackageId, prepared.Package.Version, prepared.Manifest.Product, prepared.Manifest.ProductYear, prepared.Manifest.Architecture, prepared.Manifest.RolloutChannel }, manifestSha256 = await HashAsync(Path.Combine(prepared.Package.LocalPackageRoot, "package.manifest.json"), ct), payloads = prepared.Manifest.Files.Select(x => new { path = x.Path, x.Length, x.Sha256 }), materialization = "MaterializedOrAlreadyPresentExact", activation = prepared.Activation.Outcome, pointerGeneration = prepared.Activation.Receipt?.PointerGeneration, bootstrapMechanism="StartupScript", commandLineSwitch="/b", scriptWasLastArgument=true, scriptSha256=prepared.StarterSha256, plant = new { product = "Plant3D", year = 2026, executableFingerprint = Fingerprint(product.ExecutablePath, operationId), interactive = true, batchArgumentUsed = true }, process = new { pid = process.Id, newProcessCreated=true, startupVerified = alive, packageLoadConfirmed = loaded, markerConfirmed, runtimeJournalConfirmed = runtimeConfirmed, processInteractiveAfterScript=alive }, terminalState = terminal, sideEffects = new { corporateDfsUsed = false, corporateDfsWriteAttempted = false, directDfsExecutionAttempted = false, productionProjectAccessed = false, productionPointerMutationAttempted = false, machineWideSecurityChanged=false, productionDeploymentAttempted = false } };
|
||||
var settingsSnapshot = prepared.Profile.SettingsSnapshot is null ? null : new { sourceEndpointFingerprint = prepared.Profile.SettingsSnapshot.SourceFingerprint, prepared.Profile.SettingsSnapshot.Sha256, prepared.Profile.SettingsSnapshot.Length, prepared.Profile.SettingsSnapshot.LastWriteUtc, prepared.Profile.SettingsSnapshot.AcquiredUtc, prepared.Profile.SettingsSnapshot.OfflineFallback };
|
||||
var evidence = new { schemaVersion = 3, launcherCommit = ThisCommit(), autodeskCommit = "68f5006", operationId, configuration = prepared.Profile.Name, selectedPattern = prepared.Profile.PatternName, settingsSnapshot, orderedPluginIds = new[] { prepared.Requirement.PackageId }, exactPackage = new { prepared.Package.PackageId, prepared.Package.Version, prepared.Manifest.Product, prepared.Manifest.ProductYear, prepared.Manifest.Architecture, prepared.Manifest.RolloutChannel }, manifestSha256 = await HashAsync(Path.Combine(prepared.Package.LocalPackageRoot, "package.manifest.json"), ct), payloads = prepared.Manifest.Files.Select(x => new { path = x.Path, x.Length, x.Sha256 }), materialization = "MaterializedOrAlreadyPresentExact", activation = prepared.Activation.Outcome, pointerGeneration = prepared.Activation.Receipt?.PointerGeneration, bootstrapMechanism="StartupScript", commandLineSwitch="/b", scriptWasLastArgument=true, scriptSha256=prepared.StarterSha256, plant = new { product = "Plant3D", year = 2026, executableFingerprint = Fingerprint(product.ExecutablePath, operationId), interactive = true, batchArgumentUsed = true }, process = new { pid = process.Id, newProcessCreated=true, startupVerified = alive, packageLoadConfirmed = loaded, markerConfirmed, runtimeJournalConfirmed = runtimeConfirmed, processInteractiveAfterScript=alive }, terminalState = terminal, sideEffects = new { corporateDfsUsed = false, corporateDfsWriteAttempted = false, directDfsExecutionAttempted = false, productionProjectAccessed = false, productionPointerMutationAttempted = false, machineWideSecurityChanged=false, productionDeploymentAttempted = false } };
|
||||
var evidencePath = await WriteEvidenceAsync(evidenceDirectory, evidence, ct);
|
||||
return new(process.Id, alive, loaded, evidencePath, terminal);
|
||||
}
|
||||
@@ -129,6 +137,24 @@ internal sealed class LocalParityLaunchService
|
||||
private static string Under(string root, params string[] parts) { var result = Path.GetFullPath(Path.Combine(new[] { root }.Concat(parts).ToArray())); if (!UnderRoot(root, result) && !string.Equals(Path.GetFullPath(root), result, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LOCAL_PARITY_PATH_ESCAPE"); return result; }
|
||||
private static async Task<string> HashAsync(string path, CancellationToken ct) { await using var stream = File.OpenRead(path); return Convert.ToHexString(await SHA256.HashDataAsync(stream, ct)); }
|
||||
private static string Fingerprint(string path, string salt) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(salt + "|" + path)));
|
||||
private static async Task<string> WriteEvidenceAsync(string directory, object value, CancellationToken ct) { var final = Path.Combine(directory, "controlled-local-parity-launch.json"); var temp = final + ".tmp"; await using (var stream = new FileStream(temp, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { await JsonSerializer.SerializeAsync(stream, value, cancellationToken: ct); await stream.FlushAsync(ct); } File.Move(temp, final); using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(final, ct)); if (doc.RootElement.GetProperty("schemaVersion").GetInt32() != 2) throw new InvalidOperationException("LOCAL_PARITY_EVIDENCE_INVALID"); return final; }
|
||||
private static string ThisCommit() { try { var start = new ProcessStartInfo("git", "rev-parse HEAD") { WorkingDirectory = AppContext.BaseDirectory, UseShellExecute = false, RedirectStandardOutput = true }; using var p = Process.Start(start); return p is null ? "unknown" : p.StandardOutput.ReadToEnd().Trim(); } catch { return "unknown"; } }
|
||||
private static async Task<string> WriteEvidenceAsync(string directory, object value, CancellationToken ct) { var final = Path.Combine(directory, "controlled-local-parity-launch.json"); var temp = final + ".tmp"; await using (var stream = new FileStream(temp, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { await JsonSerializer.SerializeAsync(stream, value, cancellationToken: ct); await stream.FlushAsync(ct); } File.Move(temp, final); using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(final, ct)); if (doc.RootElement.GetProperty("schemaVersion").GetInt32() != 3) throw new InvalidOperationException("LOCAL_PARITY_EVIDENCE_INVALID"); return final; }
|
||||
private static string ThisCommit()
|
||||
{
|
||||
var embedded = Environment.GetEnvironmentVariable("LAUNCHER_COMMIT");
|
||||
if (!string.IsNullOrWhiteSpace(embedded)) return embedded;
|
||||
try
|
||||
{
|
||||
var releaseManifest = Path.Combine(AppContext.BaseDirectory, "owner-review-release.json");
|
||||
if (File.Exists(releaseManifest))
|
||||
{
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(releaseManifest));
|
||||
if (document.RootElement.TryGetProperty("launcherCommit", out var commit) && !string.IsNullOrWhiteSpace(commit.GetString())) return commit.GetString()!;
|
||||
}
|
||||
var start = new ProcessStartInfo("git", "rev-parse HEAD") { WorkingDirectory = AppContext.BaseDirectory, UseShellExecute = false, RedirectStandardOutput = true };
|
||||
using var process = Process.Start(start);
|
||||
var value = process is null ? "" : process.StandardOutput.ReadToEnd().Trim();
|
||||
return string.IsNullOrWhiteSpace(value) ? "unknown" : value;
|
||||
}
|
||||
catch { return "unknown"; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Launcher.Next.Application;
|
||||
using Launcher.Next.Autodesk;
|
||||
@@ -9,113 +10,194 @@ namespace Launcher.Next.App;
|
||||
|
||||
public partial class MainWindow : System.Windows.Window
|
||||
{
|
||||
private sealed record LauncherProduct(string Id,string Name,string? Executable,string Diagnostic);
|
||||
private sealed record LauncherProduct(string Id, string Name, string? Executable, string Diagnostic);
|
||||
private readonly LocalParityLaunchProfile? localProfile;
|
||||
private readonly LocalParityLaunchService localService=new();
|
||||
private readonly LegacyConfigurationAdapter adapter=new();
|
||||
private readonly LocalParityLaunchService localService = new();
|
||||
private readonly LegacyConfigurationAdapter adapter = new();
|
||||
private LauncherProduct? selected;
|
||||
private string? projectPath;
|
||||
private string? manualRosterPath;
|
||||
public MainWindow() { InitializeComponent(); WireSourceVerifiedSelection(); Initialize(); }
|
||||
internal MainWindow(LocalParityLaunchProfile profile) { localProfile=profile; InitializeComponent(); WireSourceVerifiedSelection(); Initialize(); }
|
||||
private void WireSourceVerifiedSelection()
|
||||
{
|
||||
SelectProjectButton.Click -= SelectProject_Click;
|
||||
SelectProjectButton.Click += SelectProjectFile_Click;
|
||||
AutomaticMode.Checked -= LoadMode_Changed;
|
||||
ListMode.Checked -= LoadMode_Changed;
|
||||
AutomaticMode.Checked += LoadModeChanged;
|
||||
ListMode.Checked += LoadModeChanged;
|
||||
}
|
||||
private string stage = "ProductSelection";
|
||||
private string code = "PRODUCT_NOT_SELECTED";
|
||||
private string detail = "";
|
||||
private string pattern = "";
|
||||
private string expectedRoster = "";
|
||||
private string orderedIds = "";
|
||||
private string mappingState = "NotEvaluated";
|
||||
private string preparationState = "NotStarted";
|
||||
private string activationState = "NotStarted";
|
||||
private string bootstrapState = "NotStarted";
|
||||
private string processState = "NotStarted";
|
||||
private int? processId;
|
||||
private string packageLoadMarker = "NotStarted";
|
||||
|
||||
public MainWindow() { InitializeComponent(); Initialize(); }
|
||||
internal MainWindow(LocalParityLaunchProfile profile) { localProfile = profile; InitializeComponent(); Initialize(); }
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
var plant=new PlantProductDiscovery(new WindowsRegistryReader()).DiscoverPlant3D(2026);
|
||||
var navisworks=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),"Autodesk","Navisworks Manage 2026","Roamer.exe");
|
||||
Products.ItemsSource=new[]{new LauncherProduct("navisworks","Navisworks Manage 2026",File.Exists(navisworks)?navisworks:null,File.Exists(navisworks)?"Найден":"Navisworks Manage 2026 не найден"),new LauncherProduct("pnid","Plant P&ID 2026",plant?.ExecutablePath,plant is null?"Plant P&ID 2026 не найден":"Используется установленный Plant 3D 2026 runtime"),new LauncherProduct("plant3d","Plant 3D 2026",plant?.ExecutablePath,plant is null?"Plant 3D 2026 не найден":"Найден")};
|
||||
Version.Text="Версия: "+typeof(MainWindow).Assembly.GetName().Version;
|
||||
Status.Text="Выберите программное обеспечение.";
|
||||
if(localProfile is not null) { Products.SelectedIndex=2; Status.Text="Выберите controlled non-production папку проекта."; }
|
||||
var plant = new PlantProductDiscovery(new WindowsRegistryReader()).DiscoverPlant3D(2026);
|
||||
var navisworks = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Autodesk", "Navisworks Manage 2026", "Roamer.exe");
|
||||
Products.ItemsSource = new[]
|
||||
{
|
||||
new LauncherProduct("navisworks", "Navisworks Manage 2026", File.Exists(navisworks) ? navisworks : null, File.Exists(navisworks) ? "Найден" : "Navisworks Manage 2026 не найден"),
|
||||
new LauncherProduct("pnid", "Plant P&ID 2026", plant?.ExecutablePath, plant is null ? "Plant P&ID 2026 не найден" : "Используется установленный Plant runtime"),
|
||||
new LauncherProduct("plant3d", "Plant 3D 2026", plant?.ExecutablePath, plant is null ? "Plant 3D 2026 не найден" : "Найден")
|
||||
};
|
||||
Version.Text = "Версия: " + typeof(MainWindow).Assembly.GetName().Version;
|
||||
Transition("ProductSelection", "PRODUCT_NOT_SELECTED", "Выберите программное обеспечение");
|
||||
if (localProfile is not null) Products.SelectedIndex = 2;
|
||||
}
|
||||
private void Products_SelectionChanged(object sender,System.Windows.Controls.SelectionChangedEventArgs e)
|
||||
{ selected=Products.SelectedItem as LauncherProduct; projectPath=null; ProjectPath.Text=selected is null?"Выберите программное обеспечение":"Папка проекта не выбрана"; SelectProjectButton.IsEnabled=selected?.Executable is not null; StartButton.IsEnabled=false; Plugins.ItemsSource=null; Plugins.IsEnabled=false; ProductStatus.Text=selected?.Diagnostic??""; Status.Text=selected?.Executable is null?selected?.Diagnostic??"Выберите программное обеспечение":"Выберите папку проекта."; }
|
||||
private void SelectProject_Click(object sender,System.Windows.RoutedEventArgs e)
|
||||
|
||||
private void Products_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
||||
{
|
||||
if(selected is null)return; var dialog=new Microsoft.Win32.OpenFolderDialog { Title="Выберите папку проекта" }; if(dialog.ShowDialog()!=true)return;
|
||||
var path=Path.GetFullPath(dialog.FolderName); if(!Directory.Exists(path)){Status.Text="Выбранная папка недоступна.";return;} if(selected.Id is "plant3d" or "pnid" && !File.Exists(Path.Combine(path,"Project.xml"))){Status.Text="Выбранная папка не является проектом Plant 3D.";return;}
|
||||
projectPath=path; ProjectPath.Text=path; ResolvePlugins();
|
||||
selected = Products.SelectedItem as LauncherProduct;
|
||||
projectPath = null;
|
||||
manualRosterPath = null;
|
||||
ClearOperationState();
|
||||
ProjectPath.Text = selected is null ? "Выберите программное обеспечение" : "Проект не выбран";
|
||||
SelectProjectButton.IsEnabled = selected?.Executable is not null;
|
||||
ProductStatus.Text = selected?.Diagnostic ?? "";
|
||||
Transition(selected?.Executable is null ? "ProductSelectionFailed" : "ProjectSelection", selected?.Executable is null ? "PRODUCT_NOT_FOUND" : "PROJECT_NOT_SELECTED", selected?.Executable is null ? selected?.Diagnostic ?? "Продукт не найден" : "Выберите проект");
|
||||
}
|
||||
|
||||
private void SelectProject_Click(object sender, System.Windows.RoutedEventArgs e) => SelectProjectFile_Click(sender, e);
|
||||
|
||||
private void SelectProjectFile_Click(object sender, System.Windows.RoutedEventArgs e)
|
||||
{
|
||||
if (selected is null) return;
|
||||
var product = LegacyProductSelections.For(selected.Id);
|
||||
var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Select project", Filter = product.Filter, CheckFileExists = true, Multiselect = false };
|
||||
var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Выберите проект", Filter = product.Filter, CheckFileExists = true, Multiselect = false };
|
||||
if (dialog.ShowDialog() != true) return;
|
||||
ClearOperationState();
|
||||
try
|
||||
{
|
||||
Transition("ProjectSelected", "PROJECT_SELECTED", "Проверка проекта");
|
||||
projectPath = LegacyProductSelections.ValidateSelectedFile(product, dialog.FileName);
|
||||
manualRosterPath = null;
|
||||
ProjectPath.Text = Path.GetFileName(projectPath);
|
||||
ResolvePluginsSourceVerified();
|
||||
Transition("ProjectValidated", "PROJECT_VALID", "Поиск списка плагинов");
|
||||
ResolvePlugins();
|
||||
}
|
||||
catch (Exception ex) { Status.Text = UserMessage(ex.Message); }
|
||||
catch (Exception ex) { ReportFailure(ex, "ProjectValidationFailed"); }
|
||||
}
|
||||
|
||||
private void LoadModeChanged(object sender, System.Windows.RoutedEventArgs e)
|
||||
private void LoadMode_Changed(object sender, System.Windows.RoutedEventArgs e)
|
||||
{
|
||||
if (projectPath is null || selected is null) return;
|
||||
if (ListMode.IsChecked == true)
|
||||
{
|
||||
var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Select download_list.xml", Filter = "download_list.xml (*.xml)|*.xml|All files (*.*)|*.*", CheckFileExists = true, Multiselect = false };
|
||||
var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Выберите download_list.xml", Filter = "download_list.xml (*.xml)|*.xml|All files (*.*)|*.*", CheckFileExists = true, Multiselect = false };
|
||||
if (dialog.ShowDialog() != true) { AutomaticMode.IsChecked = true; return; }
|
||||
manualRosterPath = dialog.FileName;
|
||||
}
|
||||
else manualRosterPath = null;
|
||||
ResolvePluginsSourceVerified();
|
||||
ClearOperationState();
|
||||
ResolvePlugins();
|
||||
}
|
||||
|
||||
private void LoadMode_Changed(object sender,System.Windows.RoutedEventArgs e) { if(projectPath is not null)ResolvePlugins(); }
|
||||
private void ResolvePluginsSourceVerified()
|
||||
private void ResolvePlugins()
|
||||
{
|
||||
if (selected is null || projectPath is null) return;
|
||||
if (localProfile is not null && selected.Id == "plant3d") { ResolvePlugins(); return; }
|
||||
try
|
||||
{
|
||||
var product = LegacyProductSelections.For(selected.Id);
|
||||
pattern = localProfile is not null && selected.Id == "plant3d" ? localProfile.PatternName : product.Pattern;
|
||||
expectedRoster = manualRosterPath ?? LegacyProductSelections.AutomaticRoster(product, projectPath);
|
||||
Transition("AutomaticRosterPathResolved", "AUTOMATIC_ROSTER_PATH_RESOLVED", "Поиск списка плагинов", expectedRoster);
|
||||
var plan = LegacyProductSelections.ReadRoster(product, projectPath, manualRosterPath, adapter);
|
||||
if (localProfile is not null && selected.Id == "plant3d" && !string.Equals(Path.GetFullPath(plan.RosterPath), Path.GetFullPath(localProfile.DownloadListPath), StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException("LOCAL_PARITY_ROSTER_PROFILE_MISMATCH");
|
||||
orderedIds = string.Join(", ", plan.OrderedIds);
|
||||
Plugins.ItemsSource = plan.OrderedIds;
|
||||
Plugins.IsEnabled = false;
|
||||
PluginStatus.Text = $"List source: {plan.Source}.";
|
||||
StartButton.IsEnabled = false;
|
||||
Status.Text = plan.UnmappedIds.Count == 0
|
||||
? "Packages for the selected plugins are not mapped to 2026 packages."
|
||||
: $"Packages for {product.DisplayName} are not yet available for 2026: {string.Join(", ", plan.UnmappedIds)}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugins.ItemsSource = null;
|
||||
Plugins.IsEnabled = false;
|
||||
StartButton.IsEnabled = false;
|
||||
Status.Text = ex.Message == "LEGACY_DOWNLOAD_LIST_NOT_FOUND"
|
||||
? "Для выбранного проекта не найден список плагинов"
|
||||
: UserMessage(ex.Message);
|
||||
PluginStatus.Text = "Источник списка: " + (manualRosterPath is null ? "Автоматически" : "Выбран вручную");
|
||||
Transition("OrderedPluginIdsResolved", "PLUGIN_PLAN_RESOLVED", "Плагины определены");
|
||||
mappingState = plan.UnmappedIds.Count == 0 ? "Mapped" : "Unmapped: " + string.Join(", ", plan.UnmappedIds);
|
||||
if (plan.UnmappedIds.Count > 0)
|
||||
{
|
||||
StartButton.IsEnabled = false;
|
||||
Transition("PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND", $"Для {product.DisplayName} отсутствуют пакеты 2026: {string.Join(", ", plan.UnmappedIds)}");
|
||||
return;
|
||||
}
|
||||
if (localProfile is null || selected.Id != "plant3d")
|
||||
{
|
||||
StartButton.IsEnabled = false;
|
||||
Transition("PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND", "Для выбранных плагинов отсутствуют пакеты 2026");
|
||||
return;
|
||||
}
|
||||
ValidateLocalProfileDefinitions(plan);
|
||||
mappingState = "Mapped";
|
||||
StartButton.IsEnabled = true;
|
||||
Transition("Ready", "READY", "Готово к запуску");
|
||||
}
|
||||
catch (Exception ex) { ReportFailure(ex, "RosterResolutionFailed"); }
|
||||
}
|
||||
private void ResolvePlugins()
|
||||
|
||||
private void ValidateLocalProfileDefinitions(LegacyPluginPlan plan)
|
||||
{
|
||||
if(selected is null||projectPath is null)return;
|
||||
if(localProfile is not null && selected.Id=="plant3d")
|
||||
{
|
||||
var settings=adapter.ParseSettings(File.ReadAllText(localProfile.SettingsPath)); var pattern=settings.Patterns.Single(x=>x.Name==localProfile.PatternName); Plugins.ItemsSource=pattern.Plugins; Plugins.IsEnabled=ListMode.IsChecked==true; foreach(var item in Plugins.Items)Plugins.SelectedItems.Add(item); PluginStatus.Text=AutomaticMode.IsChecked==true?"Плагины определены автоматически для выбранного проекта.":"Выберите допустимый список плагинов."; StartButton.IsEnabled=true; Status.Text="Готово к запуску."; return;
|
||||
}
|
||||
PluginStatus.Text="Для выбранного продукта нет approved local mapping пакетов."; Plugins.ItemsSource=null; Plugins.IsEnabled=false; StartButton.IsEnabled=false; Status.Text="Источник пакета недоступен для выбранного продукта.";
|
||||
var settings = adapter.ParseSettings(File.ReadAllText(localProfile!.SettingsPath));
|
||||
var selectedPattern = settings.Patterns.SingleOrDefault(x => string.Equals(x.Name, localProfile.PatternName, StringComparison.Ordinal));
|
||||
if (selectedPattern is null) throw new InvalidOperationException("LOCAL_PARITY_PATTERN_NOT_FOUND");
|
||||
var overlay = LegacyCompatibilityOverlayParser.Parse(File.ReadAllText(localProfile.OverlayPath));
|
||||
adapter.Join(selectedPattern, new LegacyDownloadSelection(plan.OrderedIds), overlay);
|
||||
}
|
||||
private async void Start_Click(object sender,System.Windows.RoutedEventArgs e)
|
||||
|
||||
private async void Start_Click(object sender, System.Windows.RoutedEventArgs e)
|
||||
{
|
||||
if(localProfile is null||selected?.Id!="plant3d"||projectPath is null){Status.Text="Не удалось определить список плагинов.";return;}
|
||||
try { StartButton.IsEnabled=false; Status.Text="Подготовка плагинов…"; var prepared=await localService.PrepareAsync(localProfile,CancellationToken.None); Status.Text="Запуск Autodesk…"; var result=await localService.LaunchAsync(prepared,CancellationToken.None); Status.Text=result.TerminalState=="Succeeded"?$"Autodesk запущен (PID {result.ProcessId}).":"Не удалось подтвердить загрузку пакета; диагностика сохранена."; StartButton.IsEnabled=true; }
|
||||
catch(Exception ex){Status.Text="Ошибка: "+UserMessage(ex.Message);StartButton.IsEnabled=true;}
|
||||
if (localProfile is null || selected?.Id != "plant3d" || projectPath is null || !StartButton.IsEnabled) return;
|
||||
StartButton.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
preparationState = "InProgress";
|
||||
Transition("PackagePreparation", "PACKAGE_PREPARING", "Подготовка плагинов");
|
||||
var profile = manualRosterPath is null ? localProfile : localProfile with { DownloadListPath = manualRosterPath };
|
||||
var prepared = await localService.PrepareAsync(profile, CancellationToken.None);
|
||||
preparationState = "Succeeded";
|
||||
activationState = prepared.Activation.Outcome;
|
||||
bootstrapState = "Created";
|
||||
Transition("ProcessStart", "PROCESS_STARTING", "Запуск Plant 3D");
|
||||
var result = await localService.LaunchAsync(prepared, CancellationToken.None);
|
||||
processId = result.ProcessId;
|
||||
processState = result.StartupVerified ? "Started" : "ExitedEarly";
|
||||
packageLoadMarker = result.PackageLoadConfirmed ? "AssemblyInitializeCompleted" : "Missing";
|
||||
if (result.TerminalState == "Succeeded") Transition("Ready", "PACKAGE_LOAD_CONFIRMED", $"Plant 3D готов к работе (PID {result.ProcessId})");
|
||||
else Transition("PackageLoadFailed", "PACKAGE_LOAD_NOT_CONFIRMED", "Plant 3D запущен, но автоматическая загрузка плагинов не подтверждена");
|
||||
}
|
||||
catch (Exception ex) { ReportFailure(ex, "PackagePreparationFailed"); }
|
||||
finally { StartButton.IsEnabled = stage == "Ready"; }
|
||||
}
|
||||
|
||||
private void ClearOperationState()
|
||||
{
|
||||
Plugins.ItemsSource = null;
|
||||
Plugins.IsEnabled = false;
|
||||
StartButton.IsEnabled = false;
|
||||
pattern = expectedRoster = orderedIds = detail = "";
|
||||
mappingState = preparationState = activationState = bootstrapState = processState = "NotStarted";
|
||||
processId = null;
|
||||
packageLoadMarker = "NotStarted";
|
||||
}
|
||||
|
||||
private void Transition(string nextStage, string nextCode, string message, string? technicalDetail = null)
|
||||
{
|
||||
stage = nextStage;
|
||||
code = nextCode;
|
||||
detail = technicalDetail ?? "";
|
||||
Status.Text = message;
|
||||
}
|
||||
|
||||
private void ReportFailure(Exception exception, string fallbackStage)
|
||||
{
|
||||
var failure = LauncherOperationFailures.Classify(exception.Message, fallbackStage, fallbackStage == "ProcessStartFailed");
|
||||
Transition(failure.Stage, failure.Code, failure.UserMessage, exception.ToString());
|
||||
}
|
||||
|
||||
private void Help_Click(object sender, System.Windows.RoutedEventArgs e) => System.Windows.MessageBox.Show("Выберите ПО и project file. В автоматическом режиме Launcher использует список плагинов, определённый проектом. Режим \"По списку\" позволяет явно выбрать download_list.xml.", "Справка");
|
||||
|
||||
private void Diagnostics_Click(object sender, System.Windows.RoutedEventArgs e)
|
||||
{
|
||||
var report = $"Launcher version: {typeof(MainWindow).Assembly.GetName().Version}\nSelected product: {selected?.Name}\nDiscovery: {selected?.Diagnostic}\nSelected project: {projectPath}\nProject validation: {(projectPath is null ? "NotStarted" : "Passed")}\nSelected mode: {(AutomaticMode.IsChecked == true ? "Automatic" : "Manual")}\nPattern: {pattern}\nExpected download list: {expectedRoster}\nOrdered legacy IDs: {orderedIds}\nDefinition join: {mappingState}\nPreparation: {preparationState}\nActivation: {activationState}\nBootstrap: {bootstrapState}\nProcess start: {processState}\nPID: {processId}\nPackage-load marker: {packageLoadMarker}\nLast error stage: {stage}\nLast error code: {code}\nTechnical detail: {detail}";
|
||||
System.Windows.MessageBox.Show(report, "Диагностика Launcher Next");
|
||||
}
|
||||
private static string UserMessage(string code)=>code.Contains("NOT_FOUND",StringComparison.Ordinal)?"Не найден Plant 3D 2026":code.Contains("MANIFEST",StringComparison.Ordinal)?"Пакет повреждён":code.Contains("ACTIVATION",StringComparison.Ordinal)?"Не удалось активировать пакет":"Не удалось запустить Autodesk";
|
||||
private void Help_Click(object sender,System.Windows.RoutedEventArgs e)=>System.Windows.MessageBox.Show("Выберите ПО, затем папку проекта. В автоматическом режиме Launcher сам подготовит допустимые пакеты и запустит Autodesk.","Справка");
|
||||
private void Diagnostics_Click(object sender,System.Windows.RoutedEventArgs e)=>System.Windows.MessageBox.Show($"{selected?.Diagnostic}\n{ProjectPath.Text}\n{Status.Text}","Диагностика Launcher Next");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Launcher.Next.Application;
|
||||
|
||||
public sealed record LauncherOperationFailure(string Stage, string Code, string UserMessage, bool ProcessStartAttempted);
|
||||
|
||||
public static class LauncherOperationFailures
|
||||
{
|
||||
public static LauncherOperationFailure Classify(string code, string fallbackStage, bool processStartAttempted = false) => code switch
|
||||
{
|
||||
"LEGACY_DOWNLOAD_LIST_NOT_FOUND" => new("RosterResolutionFailed", "AUTOMATIC_DOWNLOAD_LIST_NOT_FOUND", "Для выбранного проекта не найден список плагинов", false),
|
||||
"LEGACY_DOWNLOAD_SELECTION_INVALID" => new("PluginPlanFailed", "DOWNLOAD_LIST_INVALID", "Список плагинов проекта содержит недопустимые или повторяющиеся записи", false),
|
||||
"LEGACY_PLUGIN_PRODUCT_MISMATCH" => new("PluginPlanFailed", "PLUGIN_PRODUCT_MISMATCH", "Список плагинов не совместим с выбранным продуктом", false),
|
||||
"LEGACY_PLUGIN_ID_NOT_DEFINED" => new("PluginPlanFailed", "PLUGIN_DEFINITION_NOT_FOUND", "Для одного из плагинов отсутствует определение", false),
|
||||
"AMBIGUOUS_LEGACY_PLUGIN_DEFINITION" => new("PluginPlanFailed", "PLUGIN_DEFINITION_AMBIGUOUS", "Определение плагина неоднозначно", false),
|
||||
"LEGACY_MAPPING_NOT_FOUND" => new("PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND", "Для выбранных плагинов отсутствуют пакеты 2026", false),
|
||||
"LOCAL_PARITY_ROSTER_PROFILE_MISMATCH" => new("RosterResolutionFailed", "OWNER_REVIEW_PROFILE_ROSTER_MISMATCH", "Контролируемая конфигурация не соответствует выбранному проекту", false),
|
||||
_ when code.Contains("PROCESS", StringComparison.Ordinal) || code.Contains("PLANT3D_2026_NOT_FOUND", StringComparison.Ordinal) => new("ProcessStartFailed", "PROCESS_START_FAILED", "Не удалось запустить Autodesk", true),
|
||||
_ when code.Contains("ACTIVATION", StringComparison.Ordinal) => new("PackageActivationFailed", "PACKAGE_ACTIVATION_FAILED", "Не удалось активировать пакет", false),
|
||||
_ when code.Contains("MANIFEST", StringComparison.Ordinal) => new("PackagePreparationFailed", "PACKAGE_MANIFEST_INVALID", "Пакет повреждён", false),
|
||||
_ => new(fallbackStage, code, "Операция не выполнена: " + code, processStartAttempted)
|
||||
};
|
||||
}
|
||||
@@ -4,8 +4,11 @@ using Launcher.Next.Core;
|
||||
|
||||
namespace Launcher.Next.Application;
|
||||
|
||||
public sealed record LegacyPluginDefinition(string Id, string Renewal, string Catalog, string Install, int DefinitionOrder);
|
||||
public sealed record LegacyProductPattern(string Name, string Filter, string Roster, IReadOnlyList<LegacyPluginDefinition> Plugins);
|
||||
public sealed record LegacyPluginDefinition(string Id, string Renewal, string Catalog, string Install, int DefinitionOrder, IReadOnlyList<string>? Machines = null)
|
||||
{
|
||||
public bool IsAllowedOn(string machineName) => Machines is null || Machines.Count == 0 || Machines.Contains(machineName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
public sealed record LegacyProductPattern(string Name, string Filter, string Roster, IReadOnlyList<LegacyPluginDefinition> Plugins, IReadOnlyList<string>? Attributes = null, IReadOnlyList<string>? Sources = null, string? Target = null, string? Loader = null, string? ObjectPath = null);
|
||||
public sealed record LegacyLauncherSettings(string Help, string CareMarker, IReadOnlyList<LegacyProductPattern> Patterns);
|
||||
public sealed record LegacyDownloadSelection(IReadOnlyList<string> PluginIds);
|
||||
public sealed record LegacyCompatibilityMapping(string LegacyId, string PackageId, string Version, string Channel);
|
||||
@@ -27,8 +30,12 @@ public sealed class LegacyConfigurationAdapter
|
||||
plugin.Element("id")?.Value ?? throw new InvalidOperationException("LEGACY_PLUGIN_ID_MISSING"),
|
||||
plugin.Element("renewal")?.Value ?? throw new InvalidOperationException("LEGACY_RENEWAL_MISSING"),
|
||||
plugin.Element("catalog")?.Value ?? throw new InvalidOperationException("LEGACY_CATALOG_MISSING"),
|
||||
plugin.Element("install")?.Value ?? throw new InvalidOperationException("LEGACY_INSTALL_MISSING"), index)).ToArray();
|
||||
return new LegacyProductPattern(Value("name"), Value("filter"), Value("roster"), plugins);
|
||||
plugin.Element("install")?.Value ?? throw new InvalidOperationException("LEGACY_INSTALL_MISSING"), index,
|
||||
plugin.Elements("machine").Select(x => x.Value.Trim()).Where(x => x.Length > 0).ToArray())).ToArray();
|
||||
return new LegacyProductPattern(Value("name"), Value("filter"), Value("roster"), plugins,
|
||||
pattern.Elements("attrib").Select(x => x.Value).ToArray(),
|
||||
pattern.Elements("source").Elements("course").Select(x => x.Value).ToArray(),
|
||||
pattern.Element("target")?.Value, pattern.Element("loader")?.Value, pattern.Element("object")?.Value);
|
||||
}).ToArray();
|
||||
return new(path.Element("help")?.Value ?? throw new InvalidOperationException("LEGACY_HELP_MISSING"), path.Element("care")?.Value ?? throw new InvalidOperationException("LEGACY_CARE_MISSING"), patterns);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Launcher.Next.Application;
|
||||
|
||||
public sealed record LegacySettingsSnapshot(string SourceEndpoint, string SourceFingerprint, string Sha256, long Length, DateTimeOffset LastWriteUtc, DateTimeOffset AcquiredUtc, string LocalSnapshotPath, bool OfflineFallback);
|
||||
|
||||
public static class LegacyNetworkConfiguration
|
||||
{
|
||||
public const string ApprovedSettingsEndpoint = @"\\phimproject.com\common\BIM\save\All_Serv_Install\Settings.xml";
|
||||
public const string LegacyBimAlias = @"N:\BIM";
|
||||
public const string ApprovedBimEndpoint = @"\\phimproject.com\common\BIM";
|
||||
|
||||
public static string ResolveApprovedAlias(string legacyPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(legacyPath)) throw new InvalidOperationException("LEGACY_NETWORK_PATH_INVALID");
|
||||
var normalized = legacyPath.Replace('/', '\\');
|
||||
if (string.Equals(normalized, LegacyBimAlias, StringComparison.OrdinalIgnoreCase)) return ApprovedBimEndpoint;
|
||||
var prefix = LegacyBimAlias + "\\";
|
||||
if (!normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LEGACY_NETWORK_ALIAS_NOT_APPROVED");
|
||||
return ApprovedBimEndpoint + normalized[(prefix.Length - 1)..];
|
||||
}
|
||||
|
||||
public static LegacySettingsSnapshot AcquireApprovedSnapshot(string endpoint, string localRoot)
|
||||
{
|
||||
if (!string.Equals(endpoint, ApprovedSettingsEndpoint, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LEGACY_SETTINGS_ENDPOINT_NOT_APPROVED");
|
||||
if (!Path.IsPathFullyQualified(endpoint) || !endpoint.StartsWith(@"\\", StringComparison.Ordinal) || endpoint.Contains('?') || endpoint.Contains('*')) throw new InvalidOperationException("LEGACY_SETTINGS_ENDPOINT_INVALID");
|
||||
if (!File.Exists(endpoint) || File.GetAttributes(endpoint).HasFlag(FileAttributes.ReparsePoint)) return OfflineSnapshot(endpoint, localRoot);
|
||||
var first = new FileInfo(endpoint);
|
||||
var sourceHashBefore = Hash(endpoint);
|
||||
var snapshots = Path.Combine(Path.GetFullPath(localRoot), "configuration-snapshots");
|
||||
Directory.CreateDirectory(snapshots);
|
||||
var temporary = Path.Combine(snapshots, Guid.NewGuid().ToString("N") + ".tmp");
|
||||
File.Copy(endpoint, temporary, false);
|
||||
var copiedHash = Hash(temporary);
|
||||
var after = new FileInfo(endpoint);
|
||||
var sourceHashAfter = Hash(endpoint);
|
||||
if (first.Length != after.Length || first.LastWriteTimeUtc != after.LastWriteTimeUtc || !string.Equals(sourceHashBefore, sourceHashAfter, StringComparison.Ordinal) || !string.Equals(copiedHash, sourceHashAfter, StringComparison.Ordinal))
|
||||
{
|
||||
File.Delete(temporary);
|
||||
throw new InvalidOperationException("LEGACY_SETTINGS_CHANGED_DURING_ACQUISITION");
|
||||
}
|
||||
var final = Path.Combine(snapshots, copiedHash + ".xml");
|
||||
if (File.Exists(final)) File.Delete(temporary); else File.Move(temporary, final);
|
||||
if (!string.Equals(Hash(final), copiedHash, StringComparison.Ordinal)) throw new InvalidOperationException("LEGACY_SETTINGS_SNAPSHOT_HASH_MISMATCH");
|
||||
return new(endpoint, Fingerprint(endpoint, copiedHash), copiedHash, first.Length, new DateTimeOffset(first.LastWriteTimeUtc), DateTimeOffset.UtcNow, final, false);
|
||||
}
|
||||
|
||||
private static LegacySettingsSnapshot OfflineSnapshot(string endpoint, string localRoot)
|
||||
{
|
||||
var snapshots = Path.Combine(Path.GetFullPath(localRoot), "configuration-snapshots");
|
||||
if (!Directory.Exists(snapshots)) throw new InvalidOperationException("LEGACY_SETTINGS_NETWORK_UNAVAILABLE");
|
||||
var candidate = Directory.GetFiles(snapshots, "*.xml").OrderByDescending(File.GetLastWriteTimeUtc).FirstOrDefault();
|
||||
if (candidate is null) throw new InvalidOperationException("LEGACY_SETTINGS_NETWORK_UNAVAILABLE");
|
||||
var info = new FileInfo(candidate);
|
||||
var hash = Hash(candidate);
|
||||
return new(endpoint, Fingerprint(endpoint, hash), hash, info.Length, new DateTimeOffset(info.LastWriteTimeUtc), DateTimeOffset.UtcNow, candidate, true);
|
||||
}
|
||||
|
||||
private static string Hash(string path) { using var stream = File.OpenRead(path); return Convert.ToHexString(SHA256.HashData(stream)); }
|
||||
private static string Fingerprint(string endpoint, string hash) => Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(endpoint + "|" + hash)));
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Owner-review network configuration
|
||||
|
||||
The only approved legacy Settings endpoint is `\\phimproject.com\common\BIM\save\All_Serv_Install\Settings.xml`. Launcher Next reads it read-only, validates it as the exact UNC file, snapshots it under the local controlled root, verifies the copied SHA-256, and parses only the local snapshot.
|
||||
|
||||
`N:\BIM\...` is supported only as a versioned alias for `\\phimproject.com\common\BIM\...`; other drive aliases are rejected. No payload is executed from the network source.
|
||||
|
||||
For the controlled Plant owner-review distribution, the real `Plant 3D_2019` definition supplies the project-relative roster template. The controlled roster contains the real Settings plugin ID `b455ee6eaed79d76f084846a60ce3284`, mapped explicitly to `P_Rec_Annot` `0.1.0` / `PRecAnnot.Plant3D2026.dll`. This is a migration mapping: its legacy `OrthoLoader.dll` name is not executed.
|
||||
|
||||
When the network endpoint is unavailable, Launcher can use only an existing validated local snapshot and identifies that condition as offline fallback in runtime evidence; it never silently substitutes a synthetic Settings file.
|
||||
@@ -0,0 +1,54 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$OutputRoot,
|
||||
[string]$SourceFixtureRoot = 'C:\dev\TEMP\local-parity-launch-20260806'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$releaseRoot = Join-Path $repoRoot 'Launcher.Next.App\bin\x64\Release\net8.0-windows'
|
||||
$output = [IO.Path]::GetFullPath($OutputRoot)
|
||||
$fixture = [IO.Path]::GetFullPath($SourceFixtureRoot)
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $releaseRoot 'Launcher.Next.App.exe'))) { throw 'OWNER_REVIEW_RELEASE_EXE_MISSING' }
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $fixture 'catalog\P_Rec_Annot\0.1.0\package.manifest.json'))) { throw 'OWNER_REVIEW_CATALOG_INPUT_MISSING' }
|
||||
if (Test-Path -LiteralPath $output) { throw 'OWNER_REVIEW_OUTPUT_ALREADY_EXISTS' }
|
||||
|
||||
$app = Join-Path $output 'app'
|
||||
$controlled = Join-Path $output 'controlled'
|
||||
New-Item -ItemType Directory -Path $app, $controlled, (Join-Path $controlled 'configuration'), (Join-Path $controlled 'project'), (Join-Path $controlled 'evidence') | Out-Null
|
||||
Copy-Item -Path (Join-Path $releaseRoot '*') -Destination $app -Recurse
|
||||
Copy-Item -LiteralPath (Join-Path $fixture 'catalog') -Destination $controlled -Recurse
|
||||
Copy-Item -LiteralPath (Join-Path $fixture 'project\Project.xml') -Destination (Join-Path $controlled 'project\Project.xml')
|
||||
|
||||
$rosterDirectory = Join-Path $controlled '_System\Plant 3D_2019\Applications'
|
||||
New-Item -ItemType Directory -Path $rosterDirectory | Out-Null
|
||||
'<root><id>b455ee6eaed79d76f084846a60ce3284</id></root>' | Set-Content -LiteralPath (Join-Path $rosterDirectory 'download_list.xml') -Encoding utf8
|
||||
'{"schemaVersion":1,"mappings":[{"mappingId":"owner-network-p-rec-annot","pattern":"Plant 3D_2019","legacyPluginId":"b455ee6eaed79d76f084846a60ce3284","legacyCatalog":"P_Rec_Annot","legacyInstall":"OrthoLoader.dll","package":{"id":"P_Rec_Annot","product":"Plant3D","productYear":2026,"architecture":"x64","channel":"controlled-local","version":"0.1.0"}}]}' | Set-Content -LiteralPath (Join-Path $controlled 'configuration\compatibility-overlay.json') -Encoding utf8
|
||||
|
||||
$profile = [ordered]@{
|
||||
schemaVersion = 1
|
||||
name = 'Owner Review Controlled Local PXP-OA'
|
||||
root = $controlled
|
||||
settingsPath = (Join-Path $controlled 'configuration\settings-pending.xml')
|
||||
downloadListPath = (Join-Path $rosterDirectory 'download_list.xml')
|
||||
overlayPath = (Join-Path $controlled 'configuration\compatibility-overlay.json')
|
||||
catalogRoot = (Join-Path $controlled 'catalog')
|
||||
patternName = 'Plant 3D_2019'
|
||||
evidenceRoot = (Join-Path $controlled 'evidence')
|
||||
networkSettingsEndpoint = '\\phimproject.com\common\BIM\save\All_Serv_Install\Settings.xml'
|
||||
}
|
||||
$profilePath = Join-Path $controlled 'configuration\owner-review-profile.json'
|
||||
$profile | ConvertTo-Json | Set-Content -LiteralPath $profilePath -Encoding utf8
|
||||
@{ launcherCommit = (git -C $repoRoot rev-parse HEAD).Trim(); distribution = 'OwnerReviewControlledLocal'; createdUtc = [DateTimeOffset]::UtcNow } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $app 'owner-review-release.json') -Encoding utf8
|
||||
@"
|
||||
@echo off
|
||||
"%~dp0Launcher.Next.App.exe" --mode LocalParityLaunch --config "%~dp0..\controlled\configuration\owner-review-profile.json"
|
||||
"@ | Set-Content -LiteralPath (Join-Path $app 'Launch Owner Review.cmd') -Encoding ascii
|
||||
|
||||
[pscustomobject]@{
|
||||
DistributionRoot = $output
|
||||
Executable = (Join-Path $app 'Launcher.Next.App.exe')
|
||||
Launcher = (Join-Path $app 'Launch Owner Review.cmd')
|
||||
Profile = $profilePath
|
||||
Project = (Join-Path $controlled 'project\Project.xml')
|
||||
AutomaticRoster = (Join-Path $rosterDirectory 'download_list.xml')
|
||||
} | Format-List
|
||||
@@ -0,0 +1,39 @@
|
||||
using Launcher.Next.Application;
|
||||
using Xunit;
|
||||
|
||||
namespace Launcher.Next.Tests;
|
||||
|
||||
public sealed class LegacyNetworkConfigurationTests
|
||||
{
|
||||
[Fact]
|
||||
public void ApprovedSettingsEndpoint_IsExactUncFile()
|
||||
{
|
||||
Assert.Equal(@"\\phimproject.com\common\BIM\save\All_Serv_Install\Settings.xml", LegacyNetworkConfiguration.ApprovedSettingsEndpoint);
|
||||
Assert.StartsWith(@"\\", LegacyNetworkConfiguration.ApprovedSettingsEndpoint, StringComparison.Ordinal);
|
||||
Assert.EndsWith("Settings.xml", LegacyNetworkConfiguration.ApprovedSettingsEndpoint, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"N:\BIM", @"\\phimproject.com\common\BIM")]
|
||||
[InlineData(@"N:\BIM\save\Apps\Plant3D\DriveA", @"\\phimproject.com\common\BIM\save\Apps\Plant3D\DriveA")]
|
||||
public void ApprovedNDriveAlias_ResolvesOnlyBimNamespace(string original, string expected)
|
||||
=> Assert.Equal(expected, LegacyNetworkConfiguration.ResolveApprovedAlias(original));
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"N:\Other\payload.dll")]
|
||||
[InlineData(@"Q:\BIM\save\Apps")]
|
||||
[InlineData("relative\\Settings.xml")]
|
||||
public void UnknownLegacyAliases_AreRejected(string path)
|
||||
=> Assert.Equal("LEGACY_NETWORK_ALIAS_NOT_APPROVED", Assert.Throws<InvalidOperationException>(() => LegacyNetworkConfiguration.ResolveApprovedAlias(path)).Message);
|
||||
|
||||
[Fact]
|
||||
public void UnapprovedEndpoint_IsRejectedBeforeAnyNetworkAccess()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "launcher-network-" + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
Assert.Equal("LEGACY_SETTINGS_ENDPOINT_NOT_APPROVED", Assert.Throws<InvalidOperationException>(() => LegacyNetworkConfiguration.AcquireApprovedSnapshot(@"\\example.invalid\Settings.xml", root)).Message);
|
||||
}
|
||||
finally { if (Directory.Exists(root)) Directory.Delete(root, true); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Launcher.Next.Application;
|
||||
using Xunit;
|
||||
|
||||
namespace Launcher.Next.Tests;
|
||||
|
||||
public sealed class LegacyNetworkSettingsParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void FullLegacyPattern_PreservesSourcesAttributesAndMachineRestriction()
|
||||
{
|
||||
const string xml = "<root><path><help>h</help><care>c</care></path><data><pattern><name>Plant 3D_2019</name><filter>Project files |Project.xml|All files (*.*)|*.*</filter><roster>%PROJECTDIRECTORY%\\..\\_System\\Plant 3D_2019\\Applications\\download_list.xml</roster><source><course>N:\\BIM\\save\\Apps\\Plant3D\\DriveA</course></source><source><course>N:\\BIM\\save\\Apps\\Plant3D\\DriveQ</course></source><target>target</target><loader>loader</loader><object>object</object><attrib>first</attrib><attrib>second</attrib></pattern><plugin><id>b455ee6eaed79d76f084846a60ce3284</id><renewal></renewal><catalog>P_Rec_Annot</catalog><install>OrthoLoader.dll</install><machine>BUILD-01</machine></plugin></data></root>";
|
||||
var settings = new LegacyConfigurationAdapter().ParseSettings(xml);
|
||||
var pattern = Assert.Single(settings.Patterns);
|
||||
Assert.Equal(["N:\\BIM\\save\\Apps\\Plant3D\\DriveA", "N:\\BIM\\save\\Apps\\Plant3D\\DriveQ"], pattern.Sources);
|
||||
Assert.Equal(["first", "second"], pattern.Attributes);
|
||||
Assert.Equal("target", pattern.Target);
|
||||
var plugin = Assert.Single(pattern.Plugins);
|
||||
Assert.Equal("P_Rec_Annot", plugin.Catalog);
|
||||
Assert.True(plugin.IsAllowedOn("build-01"));
|
||||
Assert.False(plugin.IsAllowedOn("OTHER"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PluginWithoutMachineRestriction_IsAllowedEverywhere()
|
||||
{
|
||||
var plugin = new LegacyPluginDefinition("id", "", "catalog", "install", 0);
|
||||
Assert.True(plugin.IsAllowedOn(Environment.MachineName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Launcher.Next.Application;
|
||||
using Xunit;
|
||||
|
||||
namespace Launcher.Next.Tests;
|
||||
|
||||
public sealed class OwnerReviewStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void MissingAutomaticRoster_IsRosterFailure_NotProcessStartFailure()
|
||||
{
|
||||
var failure = LauncherOperationFailures.Classify("LEGACY_DOWNLOAD_LIST_NOT_FOUND", "RosterResolutionFailed");
|
||||
Assert.Equal("RosterResolutionFailed", failure.Stage);
|
||||
Assert.Equal("AUTOMATIC_DOWNLOAD_LIST_NOT_FOUND", failure.Code);
|
||||
Assert.False(failure.ProcessStartAttempted);
|
||||
Assert.DoesNotContain("запустить Autodesk", failure.UserMessage, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("LEGACY_DOWNLOAD_SELECTION_INVALID", "PluginPlanFailed", "DOWNLOAD_LIST_INVALID")]
|
||||
[InlineData("LEGACY_PLUGIN_PRODUCT_MISMATCH", "PluginPlanFailed", "PLUGIN_PRODUCT_MISMATCH")]
|
||||
[InlineData("LEGACY_MAPPING_NOT_FOUND", "PackageMappingFailed", "PACKAGE_MAPPING_NOT_FOUND")]
|
||||
[InlineData("LOCAL_PARITY_ROSTER_PROFILE_MISMATCH", "RosterResolutionFailed", "OWNER_REVIEW_PROFILE_ROSTER_MISMATCH")]
|
||||
public void SelectionAndMappingErrors_KeepTheirOperationStage(string source, string stage, string code)
|
||||
{
|
||||
var failure = LauncherOperationFailures.Classify(source, "ProjectValidationFailed");
|
||||
Assert.Equal(stage, failure.Stage);
|
||||
Assert.Equal(code, failure.Code);
|
||||
Assert.False(failure.ProcessStartAttempted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LaunchFailure_IsAttributedOnlyToActualProcessStartCode()
|
||||
{
|
||||
var failure = LauncherOperationFailures.Classify("PLANT3D_PROCESS_NOT_CREATED", "PackagePreparationFailed");
|
||||
Assert.Equal("ProcessStartFailed", failure.Stage);
|
||||
Assert.True(failure.ProcessStartAttempted);
|
||||
Assert.Equal("Не удалось запустить Autodesk", failure.UserMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownPreparationFailure_DoesNotBecomeProcessFailure()
|
||||
{
|
||||
var failure = LauncherOperationFailures.Classify("LOCAL_PARITY_PATTERN_NOT_FOUND", "PackagePreparationFailed");
|
||||
Assert.Equal("PackagePreparationFailed", failure.Stage);
|
||||
Assert.False(failure.ProcessStartAttempted);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user