c8502bfa创建于 2025年6月25日历史提交
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
    }
}