using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
/// <summary>
/// Provides functionality to load, execute, analyze, and fix external PowerShell-based plugins.
/// </summary>
///
public static class PluginManager
{
/// 1. Execute a PowerShell script asynchronously and log output/errors.
public static async Task ExecutePlugin(string pluginPath)
{
try
{
using (var process = new Process())
{
process.StartInfo.FileName = "powershell.exe";
process.StartInfo.Arguments = $"-NoProfile -ExecutionPolicy Bypass -File \"{pluginPath}\"";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.OutputDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
Logger.Log($"[PS Output] {e.Data}");
};
process.ErrorDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
Logger.Log($"[PS Error] {e.Data}", LogLevel.Error);
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await Task.Run(() => process.WaitForExit());
Logger.Log($"✅ Script executed: {Path.GetFileName(pluginPath)}");
}
}
catch (Exception ex)
{
Logger.Log($"❌ Error executing script: {ex.Message}", LogLevel.Error);
}
}
/// 2. Load all .ps1 plugin files from the 'plugins' folder into a TreeView.
public static void LoadPlugins(TreeView treeView)
{
string pluginsFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "plugins");
if (!Directory.Exists(pluginsFolder))
{
Directory.CreateDirectory(pluginsFolder);
return;
}
var pluginsNode = new TreeNode("Plugins")
{
BackColor = Color.Magenta,
ForeColor = Color.White
};
foreach (var scriptPath in Directory.GetFiles(pluginsFolder, "*.ps1"))
{
var scriptName = Path.GetFileNameWithoutExtension(scriptPath);
var scriptNode = new TreeNode
{
Text = $"{scriptName}", // [PS]
ToolTipText = scriptPath,
Tag = scriptPath,
Checked = false
};
pluginsNode.Nodes.Add(scriptNode);
}
treeView.Nodes.Add(pluginsNode);
treeView.ExpandAll();
}
/// 3. Parse the [Commands] section from plugin content.
private static Dictionary<string, string> ParseCommands(string pluginContent)
{
return ParseSection(pluginContent, "Commands");
}
/// 4. Parse the [Expect] section from plugin content.
private static Dictionary<string, string> ParseExpect(string pluginContent)
{
return ParseSection(pluginContent, "Expect");
}
/// 5. Generic parser for named sections like [Commands] or [Expect].
/// Lines must be in 'key = value' format.
private static Dictionary<string, string> ParseSection(string content, string sectionName)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
bool insideSection = false;
foreach (var line in lines)
{
var trimmed = line.Trim();
if (trimmed.Equals($"[{sectionName}]", StringComparison.OrdinalIgnoreCase))
{
insideSection = true;
continue;
}
// Exit the section when another section begins
if (insideSection)
{
if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
break;
// Parse lines of form: key = value
var idx = trimmed.IndexOf('=');
if (idx > 0)
{
var key = trimmed.Substring(0, idx).Trim();
var val = trimmed.Substring(idx + 1).Trim();
result[key] = val;
}
}
}
return result;
}
/// 6. Execute a shell command (CMD) and return exit code and output.
private static async Task<(int exitCode, string output)> ExecuteCommand(string command)
{
var process = new Process();
var outputBuilder = new StringBuilder();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = $"/c \"{command}\"";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.OutputDataReceived += (s, e) => { if (e.Data != null) outputBuilder.AppendLine(e.Data); };
process.ErrorDataReceived += (s, e) => { if (e.Data != null) outputBuilder.AppendLine(e.Data); };
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await Task.Run(() => process.WaitForExit());
return (process.ExitCode, outputBuilder.ToString());
}
/// <summary>
/// 7. Analyzes a single plugin node by running its 'Check' command
/// and comparing the output against expected values defined in the [Expect] section.
/// Logs a summary indicating success if all checks pass, or warnings if mismatches occur.
/// Only the specified plugin node is analyzed, regardless of its checked state.
/// </summary>
/// <param name="node">The TreeNode representing the plugin to analyze, with script path stored in Tag.</param>
public static async Task AnalyzePlugin(TreeNode node)
{
if (node == null || node.Tag == null || !File.Exists(node.Tag.ToString()))
{
// Logger.Log($"❌ Script file not found for plugin: {node?.Text}", LogLevel.Error);
Logger.Log(new string('-', 50), LogLevel.Info);
}
else
{
string pluginName = node.Text;
string path = node.Tag.ToString();
string content = File.ReadAllText(path);
Dictionary<string, string> commands = ParseCommands(content);
Dictionary<string, string> expected = ParseExpect(content);
if (!commands.ContainsKey("Check"))
{
Logger.Log($"🔎 Plugin ready: [PS] {Path.GetFileName(path)}");
Logger.Log(new string('-', 50), LogLevel.Info);
}
else
{
string checkCmd = commands["Check"];
var result = await ExecuteCommand(checkCmd);
string output = result.Item2;
bool allMatched = true;
StringBuilder mismatchDetails = new StringBuilder();
foreach (var entry in expected)
{
string key = entry.Key;
string expectedVal = entry.Value;
var match = Regex.Match(output, $@"{Regex.Escape(key)}\s+REG_\w+\s+(\S+)", RegexOptions.IgnoreCase);
if (match.Success)
{
string actual = match.Groups[1].Value;
if (!expectedVal.Equals(actual, StringComparison.OrdinalIgnoreCase))
{
allMatched = false;
mismatchDetails.AppendLine($" ➤ {key}: expected '{expectedVal}', found '{actual}'");
}
}
else
{
allMatched = false;
mismatchDetails.AppendLine(
$" ➤ Warning: The registry key '{key}' could not be located in the output. " +
"This usually means the key is missing and the tweak will have to add it. " +
"[InternalCode: Could not be parsed from output]");
}
}
if (allMatched)
{
Logger.Log($"✅ Plugin: {pluginName} is properly configured.", LogLevel.Info);
node.ForeColor = Color.Gray;
}
else
{
Logger.Log($"❌ Plugin: {pluginName} requires attention.\n{mismatchDetails}", LogLevel.Warning);
node.ForeColor = Color.Red;
}
Logger.Log(new string('-', 50), LogLevel.Info);
}
}
}
/// <summary>
/// 8. Applies the fix to a single plugin node.
/// This method processes only the specified node regardless of its checked state.
/// It attempts to run the "Do" command from the plugin script, or falls back to executing the entire script.
/// </summary>
public static async Task FixPlugin(TreeNode node)
{
if (node?.Tag is string path && File.Exists(path))
{
var content = File.ReadAllText(path);
var commands = ParseCommands(content);
if (commands.TryGetValue("Do", out string doCmd))
{
Logger.Log($"🔧 Running Do command for plugin: {node.Text}");
var (exitCode, output) = await ExecuteCommand(doCmd);
Logger.Log($"Do Output:\n{output}");
Logger.Log(exitCode == 0 ? "✅ Fix applied successfully." : "❌ Fix failed.");
}
else
{
Logger.Log($"🔧 No Do command found, executing full script.");
await ExecutePlugin(path);
}
}
}
/// <summary>
/// 9. Reverts changes for a single plugin node.
/// </summary>
public static async Task RestorePlugin(TreeNode node)
{
if (node?.Tag is string path && File.Exists(path))
{
var content = File.ReadAllText(path);
var commands = ParseCommands(content);
if (commands.TryGetValue("Undo", out string undoCmd))
{
Logger.Log($"♻️ Running Undo command for plugin: {node.Text}");
var (exitCode, output) = await ExecuteCommand(undoCmd);
Logger.Log($"Undo Output:\n{output}");
Logger.Log(exitCode == 0 ? "✅ Restore successful." : "❌ Restore failed.");
}
else
{
Logger.Log($"⚠️ No Undo command found. Restore not possible.");
MessageBox.Show("Restore is not possible for this plugin.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
}
/// 10. Recursively analyze all checked plugin nodes.
public static async Task AnalyzeAll(TreeNode node)
{
if (node.Checked && node.Tag is string path && File.Exists(path))
await AnalyzePlugin(node);
foreach (TreeNode child in node.Nodes)
await AnalyzeAll(child);
}
public static async Task AnalyzeAllPlugins(TreeNodeCollection nodes)
{
Logger.Log("\n🔌 PLUGIN ANALYSIS", LogLevel.Info);
Logger.Log(new string('=', 50), LogLevel.Info);
foreach (TreeNode node in nodes)
await AnalyzeAll(node);
}
/// 11. Recursively apply fixes for all checked plugin nodes.
public static async Task FixChecked(TreeNode node)
{
if (node.Checked && node.Tag is string pluginPath)
{
var pluginName = Path.GetFileName(pluginPath);
var proceed = ShowPluginWarning(pluginName);
if (!proceed) return;
await FixPlugin(node);
}
foreach (TreeNode child in node.Nodes)
await FixChecked(child);
}
/// 12. Return true if the node represents a PowerShell plugin file.
public static bool IsPluginNode(TreeNode node)
{
return node?.Tag is string path && path.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase);
}
/// 13. Show a warning before executing external plugin code.
public static bool ShowPluginWarning(string pluginName)
{
var result = MessageBox.Show(
$"⚠️ WARNING: The plugin '{pluginName}' is an external script. Its execution is outside this app's responsibility and at your own risk.\n" +
"Proceed only if you trust the source of this plugin. Do you want to continue?",
"Plugin Activation Warning",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning
);
return result == DialogResult.Yes;
}
public static bool ShowHelp(TreeNode node)
{
string info = GetPluginHelpInfo(node);
if (!string.IsNullOrEmpty(info))
{
MessageBox.Show(info, $"Plugin Help: {node.Text}", MessageBoxButtons.OK, MessageBoxIcon.Information);
return true;
}
return false;
}
/// <summary>
/// Returns the help/info string from the plugin commands section for the given node.
/// Assumes node.Tag contains the script path as string.
/// </summary>
public static string GetPluginHelpInfo(TreeNode node)
{
if (node?.Tag is string path && File.Exists(path))
{
string content = File.ReadAllText(path);
// Simple parsing to find line starting with "Info=" under [Commands]
bool inCommandsSection = false;
foreach (var line in content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
var trimmed = line.Trim();
if (trimmed.StartsWith("[Commands]", StringComparison.OrdinalIgnoreCase))
{
inCommandsSection = true;
continue;
}
if (trimmed.StartsWith("[") && trimmed.EndsWith("]") && inCommandsSection)
{
// Left commands section
break;
}
if (inCommandsSection && trimmed.StartsWith("Info=", StringComparison.OrdinalIgnoreCase))
{
return trimmed.Substring(5).Trim(); // Return the text after "Info="
}
}
}
return null; // No info found or invalid node
}
}