要做一個系統備份、恢復系統,之前用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命名管道更方便。