src/PwX.BuildHost.PS7/Program.cs

// PwX Build Engine - PowerShell 7 host
// Gomulu payload.ps1 dosyasini benzersiz gecici klasore cikarir, PowerShell SDK ile calistirir,
// betigin cikis kodunu dondurur ve gecici klasoru temizler.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Threading;
 
namespace PwX.BuildHost
{
    internal static class Program
    {
        private const string PayloadResource = "PwX.payload.ps1";
 
        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        private static extern int MessageBoxW(IntPtr hWnd, string text, string caption, uint type);
 
        [STAThread]
        private static int Main(string[] args)
        {
            string workDir = null;
            try
            {
                ConfigureModulePath();
                workDir = Path.Combine(Path.GetTempPath(), "pwx-" + Guid.NewGuid().ToString("N"));
                Directory.CreateDirectory(workDir);
                string scriptPath = Path.Combine(workDir, "payload.ps1");
                ExtractPayload(scriptPath);
 
#if PWX_GUI
                return RunHidden(scriptPath, args);
#else
                return RunConsole(scriptPath, args);
#endif
            }
            catch (Exception ex)
            {
                ReportFatal("PwX host hatasi: " + ex.Message);
                return 1;
            }
            finally
            {
                try { if (workDir != null && Directory.Exists(workDir)) Directory.Delete(workDir, true); }
                catch { /* temizlik en iyi gayret esasli */ }
            }
        }
 
        private static void ExtractPayload(string target)
        {
            Assembly asm = Assembly.GetExecutingAssembly();
            using (Stream s = asm.GetManifestResourceStream(PayloadResource))
            {
                if (s == null) throw new InvalidOperationException("Gomulu betik bulunamadi.");
                using (FileStream fs = new FileStream(target, FileMode.CreateNew, FileAccess.Write, FileShare.None))
                    s.CopyTo(fs); // payload build sirasinda UTF-8 BOM ile yazilmistir
            }
        }
 
        private static void ConfigureModulePath()
        {
            try
            {
                string baseDir = AppDomain.CurrentDomain.BaseDirectory;
                List<string> candidatePaths = new List<string>();
 
                string mod1 = Path.Combine(baseDir, "Modules");
                if (Directory.Exists(mod1)) candidatePaths.Add(mod1);
 
                string mod2 = Path.Combine(baseDir, "runtimes", "win", "lib", "net8.0", "Modules");
                if (Directory.Exists(mod2)) candidatePaths.Add(mod2);
 
                string current = Environment.GetEnvironmentVariable("PSModulePath");
                if (!string.IsNullOrEmpty(current))
                {
                    foreach (string part in current.Split(Path.PathSeparator))
                    {
                        if (string.IsNullOrWhiteSpace(part)) continue;
                        // Windows PowerShell 5.1 (Desktop) modulleri PS7 Core ile uyumsuzdur
                        if (part.IndexOf("WindowsPowerShell", StringComparison.OrdinalIgnoreCase) >= 0) continue;
                        if (!candidatePaths.Exists(p => string.Equals(p, part, StringComparison.OrdinalIgnoreCase)))
                        {
                            candidatePaths.Add(part);
                        }
                    }
                }
 
                if (candidatePaths.Count > 0)
                {
                    Environment.SetEnvironmentVariable("PSModulePath", string.Join(Path.PathSeparator.ToString(), candidatePaths));
                }
            }
            catch { /* en iyi gayret */ }
        }
 
#if !PWX_GUI
        // Konsol modu: pwsh -File davranisi (parametreler, stdout/stderr, cikis kodu)
        private static int RunConsole(string scriptPath, string[] userArgs)
        {
#if PWX_UTF8_OUTPUT
            try { Console.OutputEncoding = new UTF8Encoding(false); } catch { }
#endif
#if PWX_NO_OUTPUT
            try { Console.SetOut(TextWriter.Null); } catch { }
#endif
 
            string[] psArgs = new string[userArgs.Length + 6];
            psArgs[0] = "-NoLogo";
            psArgs[1] = "-NoProfile";
            psArgs[2] = "-ExecutionPolicy";
            psArgs[3] = "Bypass";
            psArgs[4] = "-File";
            psArgs[5] = scriptPath;
            Array.Copy(userArgs, 0, psArgs, 6, userArgs.Length);
            InitialSessionState iss = InitialSessionState.CreateDefault2();
            iss.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Bypass;
            return Microsoft.PowerShell.ConsoleShell.Start(iss, string.Empty, string.Empty, psArgs);
        }
#endif
 
#if PWX_GUI
        // GUI modu: konsol penceresi yok; STA runspace icinde calistirir.
        // PowerShell 'exit N' komutunu PSHost.SetShouldExit uzerinden bildirir.
        private static int RunHidden(string scriptPath, string[] args)
        {
            InitialSessionState iss = InitialSessionState.CreateDefault2();
            iss.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Bypass;
            var host = new ExitCodeHost();
 
            using (Runspace rs = RunspaceFactory.CreateRunspace(host, iss))
            {
                rs.ApartmentState = ApartmentState.STA;
                rs.ThreadOptions = PSThreadOptions.UseCurrentThread;
                rs.Open();
 
                using (PowerShell ps = PowerShell.Create())
                {
                    ps.Runspace = rs;
                    // Payload metni pipeline'in ust duzey betigi olarak calisir (AddScript).
                    // PowerShell kaynak kodunda (LocalPipeline.InvokeHelper) ust duzey 'exit N',
                    // PSHost.SetShouldExit(N) cagrisina cevrilir. Betik DOSYASI olarak calistirilsaydi
                    // (AddCommand(path)) 'exit' yalnizca $LASTEXITCODE'u ayarlar ve host'a bildirilmezdi.
                    string scriptText = File.ReadAllText(scriptPath);
                    ps.AddScript(scriptText);
                    ScriptArguments.Apply(ps, scriptText, args);
 
                    try { ps.Invoke(); }
                    catch (RuntimeException ex)
                    {
                        ReportFatal(ex.Message);
                        return 1;
                    }
 
                    // Acik 'exit N' her zaman onceliklidir; betik daha once Write-Error ile
                    // sonlandirici olmayan hata yazmis olsa bile N dondurulur.
                    if (host.ShouldExit)
                        return host.ExitCode;
 
                    if (ps.HadErrors && ps.Streams.Error.Count > 0)
                    {
                        ReportFatal(ps.Streams.Error[0].ToString());
                        return 1;
                    }
 
                    // Normal bitis: pwsh -File ile ayni sekilde 0 (son yerel komutun
                    // $LASTEXITCODE degeri islem cikis koduna sizdirilmaz).
                    return 0;
                }
            }
        }
 
        private sealed class ExitCodeHost : PSHost
        {
            private readonly HeadlessHostUI _ui = new HeadlessHostUI();
            private readonly Guid _instanceId = Guid.NewGuid();
 
            internal bool ShouldExit { get; private set; }
            internal int ExitCode { get; private set; }
 
            public override string Name { get { return "PwX GUI Host"; } }
            public override Version Version { get { return typeof(PowerShell).Assembly.GetName().Version; } }
            public override Guid InstanceId { get { return _instanceId; } }
            public override PSHostUserInterface UI { get { return _ui; } }
            public override CultureInfo CurrentCulture { get { return Thread.CurrentThread.CurrentCulture; } }
            public override CultureInfo CurrentUICulture { get { return Thread.CurrentThread.CurrentUICulture; } }
 
            public override void SetShouldExit(int exitCode)
            {
                ExitCode = exitCode;
                ShouldExit = true;
            }
 
            public override void EnterNestedPrompt()
            {
                throw new NotSupportedException("GUI modunda ic ice konsol istemi desteklenmez.");
            }
 
            public override void ExitNestedPrompt() { }
            public override void NotifyBeginApplication() { }
            public override void NotifyEndApplication() { }
        }
 
        // Konsol bulunmadigi icin metinsel host ciktilari gizlidir; Write-Host hataya dusmez.
        // Read-Host/credential istemleri GUI host tarafindan desteklenmez.
        private sealed class HeadlessHostUI : PSHostUserInterface
        {
            public override PSHostRawUserInterface RawUI { get { return null; } }
            public override string ReadLine() { throw new NotSupportedException("GUI modunda Read-Host desteklenmez."); }
            public override SecureString ReadLineAsSecureString() { throw new NotSupportedException("GUI modunda parola istemi desteklenmez."); }
            public override void Write(string value) { }
            public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) { }
            public override void WriteLine(string value) { }
            public override void WriteErrorLine(string value) { }
            public override void WriteDebugLine(string message) { }
            public override void WriteProgress(long sourceId, ProgressRecord record) { }
            public override void WriteVerboseLine(string message) { }
            public override void WriteWarningLine(string message) { }
 
            public override Dictionary<string, PSObject> Prompt(string caption, string message, Collection<FieldDescription> descriptions)
            {
                throw new NotSupportedException("Interactive prompts are not supported in GUI mode.");
            }
 
            public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName)
            {
                throw new NotSupportedException("Credential prompts are not supported in GUI mode.");
            }
 
            public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options)
            {
                throw new NotSupportedException("GUI modunda kimlik bilgisi istemi desteklenmez.");
            }
 
            public override int PromptForChoice(string caption, string message, Collection<ChoiceDescription> choices, int defaultChoice)
            {
                throw new NotSupportedException("GUI modunda secim istemi desteklenmez.");
            }
        }
#endif
 
        // PWX_ERROR_LOG ortam degiskeni doluysa hata iletisi iletisim kutusu yerine bu dosyaya eklenir.
        // CI/otomasyonda GUI hostun MessageBox ile takili kalmasini onler ve testlerin hata metnini
        // dogrulamasini saglar.
        private static void ReportFatal(string message)
        {
            string log = Environment.GetEnvironmentVariable("PWX_ERROR_LOG");
            if (!string.IsNullOrEmpty(log))
            {
                try { File.AppendAllText(log, message + Environment.NewLine, new UTF8Encoding(false)); } catch { }
                return;
            }
#if PWX_GUI
            try { MessageBoxW(IntPtr.Zero, message, "PwX", 0x10); } catch { }
#else
            Console.Error.WriteLine(message);
#endif
        }
    }
}