要做一个系统备份、恢复系统,之前用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命名管道更方便。