獲取不支持重定向的Console程序的輸出【原創】

要做一個系統備份、恢復系統,之前用ImageX,但ImageX有一個大問題,就是它直接恢復系統時,會有很多checksum error,所幸dism解決了這個問題。因此換成調用dism。但發現了又一個問題:ImageX是支持輸出重定向的,只要打開開頭/scroll,可以很方便地獲取當前的進度。



而dism不支持輸出重定向,它的輸出全部在同一行上:



爲這個很傷腦筋,資料幾乎沒有。經過無數翻貼子和實驗,今天終於解決了,分享一下。希望給有同樣需要的朋友些幫助。

先說說這兩種在Console中顯示的區別:

如果是用printf、cout、Console.Write之類的函數輸出的,那麼就是支持輸出重定向的,通常是按順序輸出,當然也可以用backspace之類的方法往回刪除。而如果是用WriteConsole之類的API直接寫入Console緩衝區,那就不能重新定向,但這種輸出可以很好地控制格式。


既然知道它直接寫入了Console緩衝區,那麼從緩衝區讀出來就OK了,不過實現起來也挺不容易,以下給出C#代碼,關鍵處寫上註釋:

第一步:需要用API - ReadConsoleOutput從緩衝區讀出信息

這裏要用C#語法包裝一下API:(這裏借鑑了一篇貼子,地址忘了,好像叫《從Console屏幕截圖........》)

internal class DismWrapper
    {
//x,y - 要讀取的Console窗口的矩形區域的起點位置X,Y座標,以字符爲單位,而非像素
	//width,height - 要讀取的Console窗口的矩形區域的寬和高,以字符爲單位,而非像素
        public static IEnumerable<string> ReadFromBuffer(short x, short y, short width, short height)
        {
            IntPtr buffer = Marshal.AllocHGlobal(width * height * Marshal.SizeOf(typeof(CHAR_INFO)));
            if (buffer == null)
                throw new OutOfMemoryException();

            try
            {
                COORD coord = new COORD();
                SMALL_RECT rc = new SMALL_RECT();
                rc.Left = x;
                rc.Top = y;
                rc.Right = (short)(x + width - 1);
                rc.Bottom = (short)(y + height - 1);

                COORD size = new COORD();
                size.X = width;
                size.Y = height;

                const int STD_OUTPUT_HANDLE = -11;
                if (!ReadConsoleOutput(GetStdHandle(STD_OUTPUT_HANDLE), buffer, size, coord, ref rc))
                {
                    // 'Not enough storage is available to process this command' may be raised for buffer size > 64K (see ReadConsoleOutput doc.)
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }

                IntPtr ptr = buffer;
                for (int h = 0; h < height; h++)
                {
                    StringBuilder sb = new StringBuilder();
                    for (int w = 0; w < width; w++)
                    {
                        CHAR_INFO ci = (CHAR_INFO)Marshal.PtrToStructure(ptr, typeof(CHAR_INFO));
                        char[] chars = Console.OutputEncoding.GetChars(ci.charData);
                        sb.Append(chars[0]);
                        ptr += Marshal.SizeOf(typeof(CHAR_INFO));
                    }
                    yield return sb.ToString();
                }
            }
            finally
            {
                Marshal.FreeHGlobal(buffer);
            }
        }


        [StructLayout(LayoutKind.Sequential)]
        private struct CHAR_INFO
        {
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
            public byte[] charData;
            public short attributes;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct COORD
        {
            public short X;
            public short Y;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct SMALL_RECT
        {
            public short Left;
            public short Top;
            public short Right;
            public short Bottom;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct CONSOLE_SCREEN_BUFFER_INFO
        {
            public COORD dwSize;
            public COORD dwCursorPosition;
            public short wAttributes;
            public SMALL_RECT srWindow;
            public COORD dwMaximumWindowSize;
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool ReadConsoleOutput(IntPtr hConsoleOutput, IntPtr lpBuffer, COORD dwBufferSize, COORD dwBufferCoord, ref SMALL_RECT lpReadRegion);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr GetStdHandle(int nStdHandle);

    }

第二步:在Console中調用API
這裏的關鍵是需要在本Console窗口中顯示另一個Console程序的輸出結果,通過設計Process的StartInfo參數實現:
public class CommandCaller
    {
        private string m_Command;
        private Process m_Process;
        private StringBuilder m_Result = new StringBuilder();
        public string LastResult { get; private set; }
        private StringBuilder m_ErrorMsg = new StringBuilder();
        public string LastErrorMsg { get; private set; }
        public event RunWorkerCompletedEventHandler Exited;

        public CommandCaller(string command)
        {
            m_Command = command;
            m_Process = new Process();
            m_Process.StartInfo.FileName = command;
            m_Process.StartInfo.UseShellExecute = false;    //關鍵!
            m_Process.StartInfo.CreateNoWindow = false;    //關鍵!
            m_Process.StartInfo.WindowStyle = ProcessWindowStyle.Normal;
            m_Process.StartInfo.RedirectStandardError = true;
    //同時注意不要設置m_Process.StartInfo.RedirectStandardError 爲 true!
            m_Process.StartInfo.UserName = null;
            m_Process.StartInfo.Password = null;
            m_Process.EnableRaisingEvents = true;
            m_Process.ErrorDataReceived += new DataReceivedEventHandler(OnErrorDataReceived);
            m_Process.Exited += new EventHandler(OnExited);
        }

        private void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (!String.IsNullOrEmpty(e.Data))
            {
                m_ErrorMsg.Append(e.Data);
                LastErrorMsg = e.Data;
            }
        }

        private void OnExited(object sender, EventArgs e)
        {
            RunWorkerCompletedEventHandler _evtHandler = Exited;
            if (_evtHandler != null)
            {
                _evtHandler(sender, new RunWorkerCompletedEventArgs(null, null, false));
            }
        }

        public void Call(string parameter)
        {
            Cancel();
            try
            {
                m_Process.StartInfo.Arguments = parameter;
                m_Process.Start();

                m_Process.WaitForExit();
                m_Process.Close();
            }
            catch (Exception e)
            {
            }
        }

        public void Cancel()
        {
            try
            {
                if (!m_Process.HasExited)
                {
                    m_Process.Kill();
                }
            }
            catch (Exception e)
            {
            }
        }
    }

第三步:因爲本身是Console程序,因此不能隨便在Console中輸出結果,可以輸出到文件中,這裏我顯示在Console窗口的標題上。

static void Main(string[] args)
        {
            if (args.Length < 2 || !args[0].ToLower().Contains("dism"))
            {
                Console.WriteLine("Please enter valid path of dism.exe and its parameters.");
                return;
            }
            StringBuilder _sb = new StringBuilder();
            for (int i = 1; i < args.Length; i++)
            {
                _sb.Append(args[i]);
                _sb.Append(" ");
            }
            //示例
            //RunDism(@"d:\pe\dism.exe", @"/capture-image /ImageFile:e:\test.wim /CaptureDir:d:\bom /Name:test");
            RunDism(args[0], _sb.ToString());
            Console.ReadKey();
        }

        private static void RunDism(string command, string parameter)
        {
            Console.Clear();
            string readtext = string.Empty;
            double percentage = 0;
            Regex reg = new Regex(@"\[\=*\s*(\d{1,3}\.\d)%=*\s*\]");
            Task task = new Task(() =>
            {
                while (percentage < 99.9)
                {
                    foreach (string line in DismWrapper.ReadFromBuffer(0, 5, (short)Console.BufferWidth, 1))
                    {
                        readtext = line;
                    }
                    //Console.Title = readtext;
                    if (reg.IsMatch(readtext))
                    {
                        percentage = double.Parse(reg.Match(readtext).Groups[1].Value);
                        Console.Title = reg.Match(readtext).Groups[1].Value;
                    }
                }
                Console.WriteLine("Exit");
            });
            task.Start();

            CommandCaller _dismCaller = new CommandCaller(command);
            _dismCaller.Call(parameter);
        }

到了這裏,剩下就簡單了,要想在Windows窗體上顯示這些數據,簡單點的可以用獲取進程窗體標題的方法,複雜點可以用SendMessage、使用內存映射文件、通過共享內存DLL共享內存,當然C#用IO命名管道更方便。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章