Restore SEBPatch

This commit is contained in:
2025-06-01 11:44:20 +02:00
commit 8c656e3137
1297 changed files with 142172 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Security.Principal;
using System.Threading;
using SafeExamBrowser.Configuration.Contracts;
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal class Initialization : ProcedureStep
{
private static readonly Mutex mutex = new Mutex(true, AppConfig.SERVICE_MUTEX_NAME);
public Initialization(ProcedureContext context) : base(context)
{
}
internal override ProcedureStepResult Execute()
{
InitializeConsole();
if (IsSingleInstance() && HasAdminPrivileges() && SebNotRunning())
{
return ProcedureStepResult.Continue;
}
else
{
return ProcedureStepResult.Terminate;
}
}
internal override ProcedureStep GetNextStep()
{
return new MainMenu(Context);
}
private bool IsSingleInstance()
{
var isSingle = mutex.WaitOne(TimeSpan.Zero, true);
if (isSingle)
{
Logger.Info("There is currently no other instance running.");
}
else
{
Logger.Error("There is currently another instance running! Terminating...");
ShowError("You can only run one instance of the Reset Utility at a time! Press any key to exit...");
}
return isSingle;
}
private bool HasAdminPrivileges()
{
var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
var isAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
if (isAdmin)
{
Logger.Info($"User '{identity.Name}' is running the application with administrator privileges.");
}
else
{
Logger.Error($"User '{identity.Name}' is running the application without administrator privileges! Terminating...");
ShowError("This application must be run with administrator privileges! Press any key to exit...");
}
return isAdmin;
}
private bool SebNotRunning()
{
var isRunning = Mutex.TryOpenExisting(AppConfig.RUNTIME_MUTEX_NAME, out _);
if (isRunning)
{
Logger.Error("SEB is currently running! Terminating...");
ShowError("This application must not be run while SEB is running! Press any key to exit...");
}
else
{
Logger.Info("SEB is currently not running.");
}
return !isRunning;
}
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings.Logging;
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal class Log : ProcedureStep
{
public Log(ProcedureContext context) : base(context)
{
}
internal override ProcedureStepResult Execute()
{
var log = Logger.GetLog();
var offset = 0;
for (var key = default(ConsoleKey); key != ConsoleKey.Enter; key = Console.ReadKey(true).Key)
{
offset = UpdateOffset(key, offset, log.Count);
InitializeConsole();
PrintInstructions();
PrintLogSection(log, offset);
}
return ProcedureStepResult.Continue;
}
internal override ProcedureStep GetNextStep()
{
return new MainMenu(Context);
}
private void PrintInstructions()
{
Console.WriteLine("Use the up/down arrow keys to scroll. Press enter to return to the main menu.");
Console.WriteLine();
}
private void PrintLogSection(IList<ILogContent> log, int offset)
{
var count = 0;
foreach (var item in log)
{
if (offset > log.IndexOf(item))
{
continue;
}
if (item is ILogMessage message)
{
PrintMessage(message);
}
if (item is ILogText text)
{
PrintText(text);
}
count++;
if (Console.CursorTop >= Console.BufferHeight - 3)
{
break;
}
}
Console.SetCursorPosition(0, Console.BufferHeight - 3);
Console.WriteLine();
Console.WriteLine($"Showing entries {offset + 1} - {offset + count} of total {log.Count}.");
}
private int UpdateOffset(ConsoleKey key, int offset, int total)
{
if (key == ConsoleKey.DownArrow)
{
offset = offset == total - 1 ? offset : offset + 1;
}
if (key == ConsoleKey.UpArrow)
{
offset = offset == 0 ? offset : offset - 1;
}
return offset;
}
private void PrintMessage(ILogMessage message)
{
var date = message.DateTime.ToString("HH:mm:ss.fff");
var severity = message.Severity.ToString().ToUpper();
var threadId = message.ThreadInfo.Id < 10 ? $"0{message.ThreadInfo.Id}" : message.ThreadInfo.Id.ToString();
var threadName = message.ThreadInfo.HasName ? ": " + message.ThreadInfo.Name : string.Empty;
var threadInfo = $"[{threadId}{threadName}]";
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write($"{date} {threadInfo} - ");
Console.ForegroundColor = GetColorFor(message.Severity);
Console.WriteLine($"{severity}: { message.Message}");
Console.ForegroundColor = ForegroundColor;
}
private void PrintText(ILogText text)
{
var isHeader = text.Text.StartsWith("/* ");
var isComment = text.Text.StartsWith("# ");
if (isHeader || isComment)
{
Console.ForegroundColor = ConsoleColor.DarkGreen;
}
Console.WriteLine(text.Text);
Console.ForegroundColor = ForegroundColor;
}
private ConsoleColor GetColorFor(LogLevel severity)
{
switch (severity)
{
case LogLevel.Debug:
return ConsoleColor.DarkGray;
case LogLevel.Error:
return ConsoleColor.Red;
case LogLevel.Warning:
return ConsoleColor.DarkYellow;
default:
return ConsoleColor.DarkBlue;
}
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Linq;
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal class MainMenu : ProcedureStep
{
public MainMenu(ProcedureContext context) : base(context)
{
}
internal override ProcedureStepResult Execute()
{
InitializeConsole();
ShowMenu(Context.MainMenu.Cast<MenuOption>().ToList(), true);
return Context.MainMenu.First(o => o.IsSelected).Result;
}
internal override ProcedureStep GetNextStep()
{
return Context.MainMenu.First(o => o.IsSelected).NextStep;
}
}
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal class MainMenuOption : MenuOption
{
internal ProcedureStep NextStep { get; set; }
internal ProcedureStepResult Result { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal class MenuOption
{
internal bool IsSelected { get; set; }
internal string Text { get; set; }
public override string ToString()
{
return $"[{(IsSelected ? "x" : " ")}] {Text}";
}
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using SafeExamBrowser.Lockdown.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts;
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal class ProcedureContext
{
internal IFeatureConfigurationFactory ConfigurationFactory { get; set; }
internal Func<string, IFeatureConfigurationBackup> CreateBackup { get; set; }
internal ILogger Logger { get; set; }
internal IList<MainMenuOption> MainMenu { get; set; }
internal ISystemConfigurationUpdate Update { get; set; }
internal IUserInfo UserInfo { get; set; }
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal abstract class ProcedureStep
{
private CancellationTokenSource progressAnimationToken;
private Task progressAnimation;
protected ProcedureContext Context { get; }
protected ILogger Logger => Context.Logger;
protected ConsoleColor BackgroundColor => ConsoleColor.White;
protected ConsoleColor ForegroundColor => ConsoleColor.Black;
internal ProcedureStep(ProcedureContext context)
{
Context = context;
}
internal abstract ProcedureStepResult Execute();
internal abstract ProcedureStep GetNextStep();
protected void InitializeConsole()
{
var title = "SEB Reset Utility";
var height = Console.LargestWindowHeight > 40 ? 40 : Console.LargestWindowHeight;
var width = Console.LargestWindowWidth > 160 ? 160 : Console.LargestWindowWidth;
Console.SetBufferSize(width, height);
Console.SetWindowSize(width, height);
Console.BackgroundColor = BackgroundColor;
Console.ForegroundColor = ForegroundColor;
Console.Clear();
Console.BackgroundColor = ConsoleColor.Gray;
Console.ForegroundColor = ConsoleColor.Black;
Console.Write(new String(' ', Console.BufferWidth));
Console.Write(new String(' ', (int) Math.Floor((Console.BufferWidth - title.Length) / 2.0)));
Console.Write(title);
Console.Write(new String(' ', (int) Math.Ceiling((Console.BufferWidth - title.Length) / 2.0)));
Console.Write(new String(' ', Console.BufferWidth));
Console.BackgroundColor = BackgroundColor;
Console.ForegroundColor = ForegroundColor;
Console.SetCursorPosition(0, 4);
Console.CursorVisible = false;
}
protected void ClearLine(int top)
{
Console.SetCursorPosition(0, top);
Console.WriteLine(new String(' ', Console.BufferWidth));
Console.SetCursorPosition(0, top);
}
protected string ReadLine()
{
Console.Write("> ");
Console.CursorVisible = true;
var input = Console.ReadLine();
Console.CursorVisible = false;
return input;
}
protected void ShowError(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
Console.ForegroundColor = ForegroundColor;
Console.ReadKey();
}
protected void ShowMenu(IList<MenuOption> options, bool showInstructions = false)
{
var left = Console.CursorLeft;
var top = Console.CursorTop;
PrintMenu(options, left, top, showInstructions);
for (var key = Console.ReadKey(true).Key; key != ConsoleKey.Enter; key = Console.ReadKey(true).Key)
{
if (key == ConsoleKey.UpArrow || key == ConsoleKey.DownArrow)
{
SelectNextOption(key, options);
PrintMenu(options, left, top, showInstructions);
}
}
}
protected void ShowProgress(int current, int total)
{
var scale = Console.BufferWidth - 8.0;
var progress = Math.Floor(current * scale / total);
var remaining = Math.Ceiling((total - current) * scale / total);
Console.SetCursorPosition(0, Console.CursorTop);
Console.Write($"[{new String('■', (int) progress)}{new String('─', (int) remaining)}] {current * 100 / total}%");
}
protected void StartProgressAnimation()
{
progressAnimationToken = new CancellationTokenSource();
progressAnimation = Task.Run(new Action(ProgressAnimation));
}
protected void StopProgressAnimation()
{
progressAnimationToken?.Cancel();
progressAnimation?.Wait();
}
private void PrintMenu(IList<MenuOption> options, int left, int top, bool showInstructions)
{
Console.SetCursorPosition(left, top);
if (showInstructions)
{
Console.WriteLine("Please choose one of the following options:");
Console.WriteLine();
}
foreach (var option in options)
{
Console.WriteLine(option.ToString());
}
if (showInstructions)
{
Console.WriteLine();
Console.WriteLine("Use the up/down arrow keys and enter to navigate the menu.");
}
}
private void ProgressAnimation()
{
var length = 12;
var max = Console.BufferWidth - 8;
var min = 1;
var left = 1;
var operand = 1;
Console.Write($"[{new String('■', length)}{new String('─', max - length)}]");
while (!progressAnimationToken.IsCancellationRequested)
{
Console.SetCursorPosition(left, Console.CursorTop);
Console.Write(new String('─', length));
if (left + length > max)
{
operand = -1;
}
else if (left <= min)
{
operand = 1;
}
left += operand;
Console.SetCursorPosition(left, Console.CursorTop);
Console.Write(new String('■', length));
Thread.Sleep(20);
}
Console.SetCursorPosition(0, Console.CursorTop);
Console.WriteLine(new String(' ', Console.BufferWidth));
Console.SetCursorPosition(0, Console.CursorTop - 2);
}
private void SelectNextOption(ConsoleKey key, IList<MenuOption> options)
{
var current = options.First(o => o.IsSelected);
var currentIndex = options.IndexOf(current);
var nextIndex = default(int);
if (key == ConsoleKey.UpArrow)
{
nextIndex = --currentIndex < 0 ? options.Count - 1 : currentIndex;
}
if (key == ConsoleKey.DownArrow)
{
nextIndex = ++currentIndex == options.Count ? 0 : currentIndex;
}
var next = options.ElementAt(nextIndex);
current.IsSelected = false;
next.IsSelected = true;
}
}
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal enum ProcedureStepResult
{
Continue,
Terminate
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using SafeExamBrowser.Lockdown.Contracts;
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal class Reset : ProcedureStep
{
public Reset(ProcedureContext context) : base(context)
{
}
internal override ProcedureStepResult Execute()
{
InitializeConsole();
var success = TryGetUserInfo(out var userName, out var sid);
if (success)
{
ResetAll(userName, sid);
}
return ProcedureStepResult.Continue;
}
internal override ProcedureStep GetNextStep()
{
return new MainMenu(Context);
}
private bool TryGetUserInfo(out string userName, out string sid)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("IMPORTANT: Some configuration values are user specific. In order to reset these values, the user specified below needs to be logged in!");
Console.ForegroundColor = ForegroundColor;
Console.WriteLine();
Console.WriteLine("Please enter the name of the user for which to reset all configuration values:");
userName = ReadLine();
StartProgressAnimation();
var success = Context.UserInfo.TryGetSidForUser(userName, out sid);
StopProgressAnimation();
while (!success)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Could not find user '{userName}'!");
Console.ForegroundColor = ForegroundColor;
var tryAgain = new MenuOption { IsSelected = true, Text = "Try again" };
var mainMenu = new MenuOption { Text = "Return to main menu" };
ShowMenu(new List<MenuOption> { tryAgain, mainMenu });
if (mainMenu.IsSelected)
{
break;
}
ClearLine(Console.CursorTop - 1);
ClearLine(Console.CursorTop - 1);
ClearLine(Console.CursorTop - 1);
ClearLine(Console.CursorTop - 1);
userName = ReadLine();
success = Context.UserInfo.TryGetSidForUser(userName, out sid);
}
return success;
}
private void ResetAll(string userName, string sid)
{
var configurations = Context.ConfigurationFactory.CreateAll(Guid.NewGuid(), sid, userName);
var failed = new List<IFeatureConfiguration>();
Logger.Info($"Attempting to reset all configuration values for user '{userName}' with SID '{sid}'...");
Console.WriteLine();
Console.WriteLine("Initiating reset procedure...");
foreach (var configuration in configurations)
{
var success = configuration.Reset();
if (!success)
{
failed.Add(configuration);
}
ShowProgress(configurations.IndexOf(configuration) + 1, configurations.Count);
}
PerformUpdate();
if (failed.Any())
{
HandleFailure(failed);
}
else
{
HandleSuccess();
}
Console.WriteLine();
Console.WriteLine("Press any key to return to the main menu.");
Console.ReadKey();
}
private void PerformUpdate()
{
Console.WriteLine();
Console.WriteLine("Performing system configuration update, please wait...");
StartProgressAnimation();
Context.Update.Execute();
StopProgressAnimation();
Console.WriteLine("Update completed.");
}
private void HandleFailure(IList<IFeatureConfiguration> configurations)
{
Logger.Warn($"Failed to reset {configurations.Count} items!");
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Failed to reset {configurations.Count} items!");
foreach (var configuration in configurations)
{
Console.WriteLine($" - {configuration.GetType().Name}");
}
Console.ForegroundColor = ForegroundColor;
Console.WriteLine();
Console.WriteLine("Please consult the application log for more information.");
}
private void HandleSuccess()
{
Logger.Info("Successfully reset all changes!");
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine("Successfully reset all changes!");
Console.ForegroundColor = ForegroundColor;
}
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Lockdown.Contracts;
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal class Restore : ProcedureStep
{
private ProcedureStep next;
public Restore(ProcedureContext context) : base(context)
{
next = new MainMenu(Context);
}
internal override ProcedureStepResult Execute()
{
var filePath = $@"config\systemprofile\AppData\Local\{nameof(SafeExamBrowser)}\{AppConfig.BACKUP_FILE_NAME}";
var x86FilePath = Environment.ExpandEnvironmentVariables($@"%WINDIR%\system32\{filePath}");
var x64FilePath = Environment.ExpandEnvironmentVariables($@"%WINDIR%\SysWOW64\{filePath}");
InitializeConsole();
Logger.Info("Searching backup file...");
Logger.Debug($"x86 path => {x86FilePath}");
Logger.Debug($"x64 path => {x64FilePath}");
Console.WriteLine("Searching backup file...");
if (File.Exists(x86FilePath))
{
RestoreBackup(x86FilePath);
}
else if (File.Exists(x64FilePath))
{
RestoreBackup(x64FilePath);
}
else
{
HandleNoBackupFile();
}
return ProcedureStepResult.Continue;
}
internal override ProcedureStep GetNextStep()
{
return next;
}
private void RestoreBackup(string filePath)
{
var backup = Context.CreateBackup(filePath);
var configurations = backup.GetAllConfigurations();
var failed = new List<IFeatureConfiguration>();
Logger.Info($"Found backup file '{filePath}' with {configurations.Count} items. Initiating restore procedure...");
Console.WriteLine($"Found backup file with {configurations.Count} items.");
Console.WriteLine();
Console.WriteLine("Initiating restore procedure...");
foreach (var configuration in configurations)
{
var success = configuration.Restore();
if (success)
{
backup.Delete(configuration);
}
else
{
failed.Add(configuration);
}
ShowProgress(configurations.IndexOf(configuration) + 1, configurations.Count);
}
PerformUpdate();
if (failed.Any())
{
HandleFailure(failed);
}
else
{
HandleSuccess();
}
Console.WriteLine();
Console.WriteLine("Press any key to return to the main menu.");
Console.ReadKey();
}
private void PerformUpdate()
{
Console.WriteLine();
Console.WriteLine("Performing system configuration update, please wait...");
StartProgressAnimation();
Context.Update.Execute();
StopProgressAnimation();
Console.WriteLine("Update completed.");
}
private void HandleFailure(IList<IFeatureConfiguration> configurations)
{
Logger.Warn($"Failed to restore {configurations.Count} items!");
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Failed to restore {configurations.Count} items!");
foreach (var configuration in configurations)
{
Console.WriteLine($" - {configuration.GetType().Name}");
}
Console.ForegroundColor = ForegroundColor;
Console.WriteLine();
Console.WriteLine("Some configuration values may be user specific. In order to restore these values, the user who used SEB needs to be logged in.");
}
private void HandleSuccess()
{
Logger.Info("Successfully restored all changes!");
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine("Successfully restored all changes!");
Console.ForegroundColor = ForegroundColor;
}
private void HandleNoBackupFile()
{
var yes = new MenuOption { IsSelected = true, Text = "Yes" };
var no = new MenuOption { Text = "No" };
Logger.Warn("Could not find any backup file!");
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("Could not find any backup file!");
Console.ForegroundColor = ForegroundColor;
Console.WriteLine();
Console.WriteLine("Would you like to reset all configuration values possibly changed by SEB?");
ShowMenu(new List<MenuOption> { yes, no });
if (yes.IsSelected)
{
next = new Reset(Context);
}
Logger.Info($"The user chose {(yes.IsSelected ? "" : "not ")}to perform a reset.");
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Reflection;
namespace SafeExamBrowser.ResetUtility.Procedure
{
internal class Version : ProcedureStep
{
public Version(ProcedureContext context) : base(context)
{
}
internal override ProcedureStepResult Execute()
{
var executable = Assembly.GetExecutingAssembly();
var build = executable.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
var copyright = executable.GetCustomAttribute<AssemblyCopyrightAttribute>().Copyright;
var version = executable.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
InitializeConsole();
Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.WriteLine($"Safe Exam Browser, Version {version}");
Console.WriteLine($"Build {build}");
Console.WriteLine(copyright.Replace("©", "(c)"));
Console.WriteLine();
Console.ForegroundColor = ForegroundColor;
Console.WriteLine("Press any key to return to the main menu.");
Console.ReadKey();
return ProcedureStepResult.Continue;
}
internal override ProcedureStep GetNextStep()
{
return new MainMenu(Context);
}
}
}