2012年8月20日 星期一

使用ReadDirectoryChangesW API監控檔案系統的改變


在C++中若想要監控檔案系統改變有很多方法,可以用FindFirstChangeNotification取得檔案變更、或是Hook底層的API等方法來實現,這邊使用ReadDirectoryChangesW API來實現,該API使用前必須先加入Kernel32.lib。
image

並加入Windows.h的標頭檔
1#include "Windows.h"


這些步驟做完後在程式中就可以看到ReadDirectoryChangesW API了,其函式原型如下:
01BOOL WINAPI ReadDirectoryChangesW(
02  __in         HANDLE hDirectory,
03  __out        LPVOID lpBuffer,
04  __in         DWORD nBufferLength,
05  __in         BOOL bWatchSubtree,
06  __in         DWORD dwNotifyFilter,
07  __out_opt    LPDWORD lpBytesReturned,
08  __inout_opt  LPOVERLAPPED lpOverlapped,
09  __in_opt     LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
10);

該API必須帶入八個參數,hDirectory帶入的是要監控的目錄Handle、lpBuffer帶入的是用來回傳變動資料的空間、nBufferLength是lpBuffer空間的大小、bWatchSubtree是指定是否偵測子目錄、dwNotifyFilter是指定監控的目錄有哪些動作時需要通知、lpBytesReturned是用來回傳變動資料內含的長度、lpOverlapped可用來在非同步環境下使用重疊IO用、lpCompletionRoutine則是當監控完成或取消時所呼叫的回調函式。

其中dwNotifyFilter的值可設定的有FILE_NOTIFY_CHANGE_FILE_NAME、FILE_NOTIFY_CHANGE_DIR_NAME、FILE_NOTIFY_CHANGE_ATTRIBUTES、FILE_NOTIFY_CHANGE_SIZE、FILE_NOTIFY_CHANGE_LAST_WRITE、FILE_NOTIFY_CHANGE_LAST_ACCESS、FILE_NOTIFY_CHANGE_CREATION、與FILE_NOTIFY_CHANGE_SECURITY,詳細所代表的意義可參閱ReadDirectoryChangesW function

了解了函式原型後,就可以開始進入實際的使用。剛有提到說在ReadDirectoryChangesW API函式必須要帶入的第一個參數是要監控的目錄Handle,所以我們必須透過CreateFile API取得要監控的目錄Handle,像是下面這樣:
01HANDLE  hDirectoryHandle    = NULL;
02 
03 
04hDirectoryHandle = ::CreateFileA(
05    file,                  
06    FILE_LIST_DIRECTORY,               
07    FILE_SHARE_READ                    
08    | FILE_SHARE_WRITE
09    | FILE_SHARE_DELETE,
10    NULL,                              
11    OPEN_EXISTING,                     
12    FILE_FLAG_BACKUP_SEMANTICS         
13    | FILE_FLAG_OVERLAPPED,
14    NULL); 
15 
16if(!hDirectoryHandle)
17    return;

取得監控的目錄Handle後,將其帶入ReadDirectoryChangesw API,順帶帶入像是回傳變動資料的Buffer空間、與要監控的變動類型等必要參數。像是下面這樣:
01int     nBufferSize         = 1024;
02char*   buffer              = new char[nBufferSize];
03DWORD dwBytes = 0;
04 
05memset(buffer, 0, nBufferSize);
06 
07if(!::ReadDirectoryChangesW(
08    hDirectoryHandle,                      
09    buffer,                            
10    nBufferSize,       
11    bIncludeSubdirectories,            
12    FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_FILE_NAME,    
13    &dwBytes,                        
14    NULL,                   
15    NULL) || GetLastError() == ERROR_INVALID_HANDLE)
16{
17    break;
18}
19 
20if(!dwBytes)
21{
22    printf("Buffer overflow~~\r\n");
23}

這邊需注意到的是,若是變動的資料太多,提供的存儲空間不足以存放時,回傳的變動資料長度會是0,此時所有變動資料都會丟失。這樣的情況多半只會出在一瞬間大量的變動,可以增大存儲空間或是減少監控的變動類型,以減少回傳的資料量,避免溢位的發生。

若是運行沒發生問題,變動的資料會存放在當初塞進去的存儲空間,該空間的資料其實是FILE_NOTIFY_INFORMATION structure的型態存在,因此我們可將存儲空間的資料轉換成PFILE_NOTIFY_INFORMATION。裡面的Action是我們所關注的變動類型,FileName是變動的檔案名稱,檔案名稱的部分是沒有結尾符號的,必須要搭配FileNameLength去截取。另外變動的資料有時候不止一筆,因此我們必須在這邊用迴圈搭配NextEntryOffset去重覆運行處理流程,處理所有變動的資料。
01PFILE_NOTIFY_INFORMATION record = (PFILE_NOTIFY_INFORMATION)buffer;
02DWORD cbOffset = 0;
03 
04do
05{  
06    switch (record->Action)
07    {
08    case FILE_ACTION_ADDED:
09        printf("FILE_ACTION_ADDED:");
10        break;
11    case FILE_ACTION_REMOVED:
12        printf("FILE_ACTION_REMOVED:");
13        break;
14    case FILE_ACTION_MODIFIED:
15        printf("FILE_ACTION_MODIFIED:");
16        break;
17    case FILE_ACTION_RENAMED_OLD_NAME:
18        printf("FILE_ACTION_RENAMED_OLD_NAME:");               
19        break;
20 
21    case FILE_ACTION_RENAMED_NEW_NAME:
22        printf("FILE_ACTION_RENAMED_NEW_NAME:");
23        break;
24 
25    default:
26        break;
27    }      
28 
29    char fileBuffer[512];
30 
31    WideCharToMultiByte(CP_ACP, 0, record->FileName, record->FileNameLength, fileBuffer, record->FileNameLength, NULL, NULL); 
32    printf(fileBuffer);
33    printf("\r\n");
34 
35    cbOffset = record->NextEntryOffset;
36    record = (PFILE_NOTIFY_INFORMATION)((LPBYTE) record + cbOffset);
37}while(cbOffset);  

這邊示範一個簡易的使用範例,實際使用時最好還是搭配執行緒處理:
001// ConsoleApplication10.cpp : Defines the entry point for the console application.
002//
003 
004#include "stdafx.h"
005#include "Windows.h"
006 
007void MonitorDir(char* file, bool bIncludeSubdirectories = false)
008{
009    int     nBufferSize         = 1024;
010    char*   buffer              = new char[nBufferSize];   
011    HANDLE  hDirectoryHandle    = NULL;
012 
013 
014    hDirectoryHandle = ::CreateFileA(
015        file,                  
016        FILE_LIST_DIRECTORY,               
017        FILE_SHARE_READ                    
018        | FILE_SHARE_WRITE
019        | FILE_SHARE_DELETE,
020        NULL,                              
021        OPEN_EXISTING,                     
022        FILE_FLAG_BACKUP_SEMANTICS         
023        | FILE_FLAG_OVERLAPPED,
024        NULL); 
025 
026    if(!hDirectoryHandle)
027        return;
028 
029    while(1)
030    {
031        DWORD dwBytes = 0;
032 
033        memset(buffer, 0, nBufferSize);
034 
035        if(!::ReadDirectoryChangesW(
036            hDirectoryHandle,                      
037            buffer,                            
038            nBufferSize,       
039            bIncludeSubdirectories,            
040            FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_FILE_NAME,    
041            &dwBytes,                        
042            NULL,                   
043            NULL) || GetLastError() == ERROR_INVALID_HANDLE)
044        {
045            break;
046        }
047 
048        if(!dwBytes)
049        {
050            printf("Buffer overflow~~\r\n");
051        }
052         
053        PFILE_NOTIFY_INFORMATION record = (PFILE_NOTIFY_INFORMATION)buffer;
054        DWORD cbOffset = 0;
055 
056        do
057        {  
058            switch (record->Action)
059            {
060            case FILE_ACTION_ADDED:
061                printf("FILE_ACTION_ADDED:");
062                break;
063            case FILE_ACTION_REMOVED:
064                printf("FILE_ACTION_REMOVED:");
065                break;
066            case FILE_ACTION_MODIFIED:
067                printf("FILE_ACTION_MODIFIED:");
068                break;
069            case FILE_ACTION_RENAMED_OLD_NAME:
070                printf("FILE_ACTION_RENAMED_OLD_NAME:");               
071                break;
072 
073            case FILE_ACTION_RENAMED_NEW_NAME:
074                printf("FILE_ACTION_RENAMED_NEW_NAME:");
075                break;
076 
077            default:
078                break;
079            }      
080 
081            char fileBuffer[512];
082 
083            WideCharToMultiByte(CP_ACP, 0, record->FileName, record->FileNameLength, fileBuffer, record->FileNameLength, NULL, NULL); 
084            printf(fileBuffer);
085            printf("\r\n");
086 
087            cbOffset = record->NextEntryOffset;
088            record = (PFILE_NOTIFY_INFORMATION)((LPBYTE) record + cbOffset);
089        }while(cbOffset);      
090    }
091 
092    delete buffer;
093 
094    if(hDirectoryHandle)
095        CloseHandle(hDirectoryHandle);
096}
097 
098int _tmain(int argc, _TCHAR* argv[])
099{
100    MonitorDir("C:\\Users\\larry\\Desktop\\新增資料夾");
101    return 0;
102}

運行後去對監控的目錄操作~可得到類似如下的結果:
image

2012年8月16日 星期四

如何利用 7-zip 設定自動解壓縮並解壓縮後自動啟動程式


我去年有弄一個 WinForm 的案子,案子完成後要給客戶安裝檔,程式還蠻簡單的,但除了主執行檔外,還有包括幾個相關的檔案,因為我不打算用 Setup Project 進行部署,而想直接做成「綠色軟體」,讓程式點兩下即可自動進行解壓縮並執行程式。那時就決定用 7-zip 來實做,因為真的容易很多,以下是當時的筆記。
雖然可以透過 7-zip 做出內含解壓縮程式的執行檔,但透過介面最多只能做到將檔案壓縮而已,若要解壓縮後自動執行裡面的其中一個程式好像不行。
7-zip: Add to Archive
若要做出可自動解壓縮並自動執行程式的話,必須使用 SFX 模組(module)才行,又因為我希望整個流程可以自動化,所以我打算用指令列(Command Line)的方式進行設定,並且整合到 Visual Studio 的建置流程(Build Process)中,整個過程完全自動化( 建置 --> 壓縮 --> 製作成 SFX 執行檔 )。
以下是設定的過程:
1. 先到 7-zip 下載頁 下載 7z Library, SFXs for installers, Plugin for FAR Manager 回來
到 7-zip 下載頁下載 7z Library, SFXs for installers, Plugin for FAR Manager
2. 接著,你只要解壓縮 7zr.exe 與 7zSD.sfx 兩個檔案就可以了。其中的 Installer 目錄包含使用範例,看批次檔就大概知道怎麼做了。而我通常都把檔案解壓縮到 WinForm 專案的 bin 目錄下,之後我會將 Debug 或 Release 目錄整個壓縮起來即可。
7-zip: 解壓縮 7zr.exe 與 7zSD.sfx 兩個檔案
3. 然後新增一個文字檔 config.txt ,這是設定 SFX for installers 在執行的時候的行為,包括解壓縮時要顯示什麼標題(Title)、是否要提示使用者按下確認後進行解壓縮(BeginPrompt)、或解壓縮後要自動執行哪支程式(RunProgram)、...等等。以下是設定檔範例(備註:檔案請使用 UTF-8 編碼存檔)
;!@Install@!UTF-8!

Title="多奇資料庫管理系統"
BeginPrompt="您要執行 多奇資料庫管理系統 嗎?"
RunProgram="Debug\多奇資料庫管理系統.exe"

;!@InstallEnd@!
4. 再來,就可以先透過 7zr.exe 程式將檔案壓縮成 7z 檔。
7zr.exe a Debug.7z Debug\
5. 最後一個步驟,就是將 7zSD.sfxconfig.txt、以及 Debug.7z 三個檔案合併成一個可執行檔,我們可以利用 Windows 內建的 copy 指令搭配 /B 參數完成這三個檔案的合併動作。如下指令中,最後一個參數是最後要輸出的執行檔名稱。
copy /b 7zSD.sfx + config.txt + Debug.7z Debug.exe
此時,你只要執行 Debug.exe 就會出現以下畫面,在按下 是(Y) 之後就會立即執行解壓縮動作 (解壓縮到系統暫存目錄),並且在執行完後執行你在 config.txt 中定義的 Debug\多奇資料庫管理系統.exe 程式。
多奇資料庫管理系統 64% Extracting
---
以上的第4、第5個步驟可以寫成批次檔(Batch),假設我命名成 SFXDebug.bat
接著再到 Visual Studio 中的 Project Property -> Build Events -> Post-build event command line 輸入以下指令:
Visual Studio -> Project Property -> Build Events -> Post-build event command line
注意:由於該指令預設開始位置為專案建置的輸出目錄,所以我先用 cd .. 先跳到上一層目錄,再執行我的批次檔進行壓縮並產生 SFX 安裝程式,否則批次檔在執行的時候很容易會有找不到檔案的問題。
對於 SFX 的完整資訊可以下載 7-Zip Command Line Version 後,裡面有個 7-zip.chm 說明檔,這裡會有相當詳細的說明。
7-Zip Command Line Version :: 7-zip.chm Help file for SFX

2012年8月15日 星期三

在十六進位字串和數字型別間轉換


本主題中的範例會示範如何執行下列工作:
  • 取得 string 中每個字元的十六進位值。
  • 取得對應至十六進位 string 中每個值的 char
  • 將十六進位 string 轉換為 int
  • 將十六進位 string 轉換為 float
  • 將 byte 陣列轉換為十六進位 string
這個範例會輸出 string 中每個字元的十六進位值。 首先,會將 string 剖析至字元陣列。 然後,會呼叫每個字元上的 ToInt32(Char) 取得其數值。 最後,它會在 string 中將數字格式化為十六進位表示。
string input = "Hello World!";
char[] values = input.ToCharArray();
foreach (char letter in values)
{
    // Get the integral value of the character.
    int value = Convert.ToInt32(letter);
    // Convert the decimal value to a hexadecimal value in string form.
    string hexOutput = String.Format("{0:X}", value);
    Console.WriteLine("Hexadecimal value of {0} is {1}", letter, hexOutput);
}
/* Output:
   Hexadecimal value of H is 48
    Hexadecimal value of e is 65
    Hexadecimal value of l is 6C
    Hexadecimal value of l is 6C
    Hexadecimal value of o is 6F
    Hexadecimal value of   is 20
    Hexadecimal value of W is 57
    Hexadecimal value of o is 6F
    Hexadecimal value of r is 72
    Hexadecimal value of l is 6C
    Hexadecimal value of d is 64
    Hexadecimal value of ! is 21
 */


這個範例會剖析十六進位值的 string,並輸出對應至每個十六進位值的字元。 首先,呼叫 Split(Char[]) 方法取得每個十六進位值,做為陣列中的個別 string 然後呼叫ToInt32(String, Int32),將十六進位值轉換為以 int 表示的十進位值。 會顯示兩種不同的方法,以取得對應至該字元程式碼的字元。 第一個技術會使用 ConvertFromUtf32(Int32),傳回對應至整數引數的字元做為 string 第二個技術會明確將 int 轉型為 char
string hexValues = "48 65 6C 6C 6F 20 57 6F 72 6C 64 21";
string[] hexValuesSplit = hexValues.Split(' ');
foreach (String hex in hexValuesSplit)
{
    // Convert the number expressed in base-16 to an integer.
    int value = Convert.ToInt32(hex, 16);
    // Get the character corresponding to the integral value.
    string stringValue = Char.ConvertFromUtf32(value);
    char charValue = (char)value;
    Console.WriteLine("hexadecimal value = {0}, int value = {1}, char value = {2} or {3}",
                        hex, value, stringValue, charValue);
}
/* Output:
    hexadecimal value = 48, int value = 72, char value = H or H
    hexadecimal value = 65, int value = 101, char value = e or e
    hexadecimal value = 6C, int value = 108, char value = l or l
    hexadecimal value = 6C, int value = 108, char value = l or l
    hexadecimal value = 6F, int value = 111, char value = o or o
    hexadecimal value = 20, int value = 32, char value =   or
    hexadecimal value = 57, int value = 87, char value = W or W
    hexadecimal value = 6F, int value = 111, char value = o or o
    hexadecimal value = 72, int value = 114, char value = r or r
    hexadecimal value = 6C, int value = 108, char value = l or l
    hexadecimal value = 64, int value = 100, char value = d or d
    hexadecimal value = 21, int value = 33, char value = ! or !
*/


這個範例會示範另一個方法,經由呼叫 Parse(String, NumberStyles) 方法,將十六進位 string 轉換為整數。
string hexString = "8E2";
int num = Int32.Parse(hexString, System.Globalization.NumberStyles.HexNumber);
Console.WriteLine(num);
//Output: 2274


下列範例會示範如何使用 System.BitConverter 類別和 Int32.Parse 方法,將十六進位 string 轉換為float
string hexString = "43480170";
uint num = uint.Parse(hexString, System.Globalization.NumberStyles.AllowHexSpecifier);

byte[] floatVals = BitConverter.GetBytes(num);
float f = BitConverter.ToSingle(floatVals, 0);
Console.WriteLine("float convert = {0}", f);

// Output: 200.0056            


下列範例會示範如何使用 System.BitConverter 類別,將 byte 陣列轉換為十六進位字串。
byte[] vals = { 0x01, 0xAA, 0xB1, 0xDC, 0x10, 0xDD };

string str = BitConverter.ToString(vals);
Console.WriteLine(str);

str = BitConverter.ToString(vals).Replace("-", "");
Console.WriteLine(str);

/*Output:
  01-AA-B1-DC-10-DD
  01AAB1DC10DD
 */


#ifndef, #define, #endif的用法(整理)


文件中的#ifndef
頭件的中的#ifndef,這是一個很關鍵的東西。比如你有兩個C文件,這兩個C文件都include了同一個頭文件。而編譯時,這兩個C文件要一同編譯成一個可運行文件,於是問題來了,大量的聲明衝突。
還是把頭文件的內容都放在#ifndef和#endif中吧。不管你的頭文件會不會被多個文件引用,你都要加上這個。一般格式是這樣的:
#ifndef <標識>
#define <標識>
......
......
#endif
<標識>在理論上來說可以是自由命名的,但每個頭文件的這個「標識」都應該是唯一的。標識的命名規則一般是頭文件名全大寫,前後加下劃線,並把文件名中的「.」也變成下劃線,如:stdio.h
#ifndef _STDIO_H_
#define _STDIO_H_
......
#endif
2.在#ifndef中定義變量出現的問題(一般不定義在#ifndef中)。
#ifndef AAA
#define AAA
...
int i;
...
#endif
裡面有一個變量定義
在vc中鏈接時就出現了i重複定義的錯誤,而在c中成功編譯。
結論:
(1).當你第一個使用這個頭的.cpp文件生成.obj的時候,int i 在裡面定義了當另外一個使用這個的.cpp再次[單獨]生成.obj的時候,int i 又被定義然後兩個obj被另外一個.cpp也include 這個頭的,連接在一起,就會出現重複定義.
(2).把源程序文件擴展名改成.c後,VC按照C語言的語法對源程序進行編譯,而不是C++。在C語言中,若是遇到多個int i,則自動認為其中一個是定義,其他的是聲明。
(3).C語言和C++語言連接結果不同,可能(猜測)時在進行編譯的時候,C++語言將全局
變量默認為強符號,所以連接出錯。C語言則依照是否初始化進行強弱的判斷的。(參考)
解決方法:
(1).把源程序文件擴展名改成.c。
(2).推薦解決方案:
.h中只聲明 extern int i;在.cpp中定義
<x.h>
#ifndef __X_H__
#define __X_H__
extern int i;
#endif //__X_H__
<x.c>
int i;
注意問題:
(1).變量一般不要定義在.h文件中。

-------------------------------------------------------------------------------------------------------------------------------------------
一般情況下,源程序中所有的行都參加編譯。但是有時希望對其中一部分內容只在滿足一定條件才進行編譯,也就是對一部分內容指定編譯的條件,這就是「條件編譯」。有時,希望當滿足某條件時對一組語句進行編譯,而當條件不滿足時則編譯另一組語句。
條件編譯命令最常見的形式為:
    #ifdef 標識符
    程序段1
    #else
    程序段2
    #endif
    
    它的作用是:當標識符已經被定義過(一般是用#define命令定義),則對程序段1進行編譯,否則編譯程序段2。
    其中#else部分也可以沒有,即:
    #ifdef
    程序段1
    #denif
    
    這裡的「程序段」可以是語句組,也可以是命令行。這種條件編譯可以提高C源程序的通用性。如果一個C源程序在不同計算機系統上系統上運行,而不同的計算機 又有一定的差異。例如,我們有一個數據類型,在Windows平台中,應該使用long類型表示,而在其他平台應該使用float表示,這樣往往需要對源 程序作必要的修改,這就降低了程序的通用性。可以用以下的條件編譯:
    #ifdef WINDOWS
    #define MYTYPE long
    #else
    #define MYTYPE float
    #endif
    
    如果在Windows上編譯程序,則可以在程序的開始加上
    #define WINDOWS
    
    這樣則編譯下面的命令行:
    #define MYTYPE long
    
    如果在這組條件編譯命令之前曾出現以下命令行:
    #define WINDOWS 0
    
    則預編譯後程序中的MYTYPE都用float代替。這樣,源程序可以不必作任何修改就可以用於不同類型的計算機系統。當然以上介紹的只是一種簡單的情況,可以根據此思路設計出其它的條件編譯。
    例如,在調試程序時,常常希望輸出一些所需的信息,而在調試完成後不再輸出這些信息。可以在源程序中插入以下的條件編譯段:
    #ifdef DEBUG
    print ("device_open(%p) ", file);
    #endif
    
    如果在它的前面有以下命令行:
    #define DEBUG
    
    則在程序運行時輸出file指針的值,以便調試分析。調試完成後只需將這個define命令行刪除即可。有人可能覺得不用條件編譯也可達此目的,即在調試 時加一批printf語句,調試後一一將printf語句刪除去。的確,這是可以的。但是,當調試時加的printf語句比較多時,修改的工作量是很大 的。用條件編譯,則不必一一刪改printf語句,只需刪除前面的一條「#define DEBUG」命令即可,這時所有的用DEBUG作標識符的條件編譯段都使其中的printf語句不起作用,即起統一控制的作用,如同一個「開關」一樣。
    有時也採用下面的形式:
    #ifndef 標識符
    程序段1
    #else
    程序段2
    #endif
    
    只是第一行與第一種形式不同:將「ifdef」改為「ifndef」。它的作用是:若標識符未被定義則編譯程序段1,否則編譯程序段2。這種形式與第一種形式的作用相反。
    以上兩種形式用法差不多,根據需要任選一種,視方便而定。
    還有一種形式,就是#if後面的是一個表達式,而不是一個簡單的標識符:
    #if 表達式
    程序段1
    #else
    程序段2
    #endif
    
    它的作用是:當指定的表達式值為真(非零)時就編譯程序段1,否則編譯程序段2。可以事先給定一定條件,使程序在不同的條件下執行不同的功能。
---------------------------------------------------------------------------------------------------------------------------------------
作用範圍就是當前文件啊。因為編譯是以cpp或c文件位單位的嘛。還以這個為例:

//正常代碼
#ifdef _DEBUG
     TRACE("Some infomation");
#else
     //Now is release version,so do nothing
#endif
//正常代碼

編譯時是先把所有的預編譯處理展開(比如宏)再編譯,所以Debug模式下,編譯時的代碼是:
//正常代碼
TRACE("Some infomation");
//正常代碼

Release模式下的代碼是:
//正常代碼
//正常代碼