-
-
Save jannson/cf7f9ae8874ba6e89bed8a7a11f89548 to your computer and use it in GitHub Desktop.
Windows USN Journal sample in Go based on Jeffrey Richter's superb MSDN Journal article. A work in progress, intended to provide similar API to go.fsevents.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// File: fsevents_windows.go | |
// Date: October 29, 2013 | |
// Author: Peter Krnjevic <[email protected]>, on the shoulders of many others | |
// | |
// This code sample is released into the Public Domain. | |
// | |
package fsevents | |
import ( | |
// "bytes" | |
// "encoding/binary" | |
"fmt" | |
"reflect" | |
"syscall" | |
"unsafe" | |
) | |
import ( | |
// "github.com/lxn/go-winapi" | |
"github.com/lxn/walk" | |
) | |
type ( | |
WCHAR uint16 | |
WORD uint16 | |
DWORD uint32 | |
DWORDLONG uint64 | |
LONGLONG int64 | |
USN int64 | |
LARGE_INTEGER LONGLONG | |
) | |
type USN_JOURNAL_DATA struct { | |
UsnJournalID DWORDLONG | |
FirstUsn USN | |
NextUsn USN | |
LowestValidUsn USN | |
MaxUsn USN | |
MaximumSize DWORDLONG | |
AllocationDelta DWORDLONG | |
} | |
type READ_USN_JOURNAL_DATA struct { | |
StartUsn USN | |
ReasonMask DWORD | |
ReturnOnlyOnClose DWORD | |
Timeout DWORDLONG | |
BytesToWaitFor DWORDLONG | |
UsnJournalID DWORDLONG | |
} | |
type USN_RECORD struct { | |
RecordLength DWORD | |
MajorVersion WORD | |
MinorVersion WORD | |
FileReferenceNumber DWORDLONG | |
ParentFileReferenceNumber DWORDLONG | |
Usn USN | |
TimeStamp LARGE_INTEGER | |
Reason DWORD | |
SourceInfo DWORD | |
SecurityId DWORD | |
FileAttributes DWORD | |
FileNameLength WORD | |
FileNameOffset WORD | |
FileName [1]WCHAR | |
} | |
type MFT_ENUM_DATA struct { | |
StartFileReferenceNumber DWORDLONG | |
LowUsn USN | |
HighUsn USN | |
} | |
const ( | |
FSCTL_ENUM_USN_DATA = 0x900B3 | |
FSCTL_QUERY_USN_JOURNAL = 0x900F4 | |
FSCTL_READ_USN_JOURNAL = 0x900BB | |
O_RDONLY = syscall.O_RDONLY | |
O_RDWR = syscall.O_RDWR | |
O_CREAT = syscall.O_CREAT | |
O_WRONLY = syscall.O_WRONLY | |
GENERIC_READ = syscall.GENERIC_READ | |
GENERIC_WRITE = syscall.GENERIC_WRITE | |
FILE_APPEND_DATA = syscall.FILE_APPEND_DATA | |
FILE_SHARE_READ = syscall.FILE_SHARE_READ | |
FILE_SHARE_WRITE = syscall.FILE_SHARE_WRITE | |
ERROR_FILE_NOT_FOUND = syscall.ERROR_FILE_NOT_FOUND | |
O_APPEND = syscall.O_APPEND | |
O_CLOEXEC = syscall.O_CLOEXEC | |
O_EXCL = syscall.O_EXCL | |
O_TRUNC = syscall.O_TRUNC | |
CREATE_ALWAYS = syscall.CREATE_ALWAYS | |
CREATE_NEW = syscall.CREATE_NEW | |
OPEN_ALWAYS = syscall.OPEN_ALWAYS | |
TRUNCATE_EXISTING = syscall.TRUNCATE_EXISTING | |
OPEN_EXISTING = syscall.OPEN_EXISTING | |
FILE_ATTRIBUTE_NORMAL = syscall.FILE_ATTRIBUTE_NORMAL | |
FILE_FLAG_BACKUP_SEMANTICS = syscall.FILE_FLAG_BACKUP_SEMANTICS | |
FILE_ATTRIBUTE_DIRECTORY = syscall.FILE_ATTRIBUTE_DIRECTORY | |
MAX_LONG_PATH = syscall.MAX_LONG_PATH | |
) | |
var ( | |
modkernel32 = syscall.NewLazyDLL("kernel32.dll") | |
procDeviceIoControl = modkernel32.NewProc("DeviceIoControl") | |
usnJournalData USN_JOURNAL_DATA | |
readUsnJournalData READ_USN_JOURNAL_DATA | |
cb int | |
) | |
func getPointer(i interface{}) (pointer, size uintptr) { | |
v := reflect.ValueOf(i) | |
switch k := v.Kind(); k { | |
case reflect.Ptr: | |
t := v.Elem().Type() | |
size = t.Size() | |
pointer = v.Pointer() | |
case reflect.Slice: | |
size = uintptr(v.Cap()) | |
pointer = v.Pointer() | |
default: | |
fmt.Println("oops") | |
} | |
return | |
} | |
func DeviceIoControl(handle syscall.Handle, controlCode uint32, in interface{}, out interface{}, done *uint32) (err error) { | |
inPtr, inSize := getPointer(in) | |
outPtr, outSize := getPointer(out) | |
r1, _, e1 := syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), uintptr(controlCode), inPtr, uintptr(inSize), outPtr, uintptr(outSize), uintptr(unsafe.Pointer(done)), uintptr(0), 0) | |
if r1 == 0 { | |
if e1 != 0 { | |
err = error(e1) | |
} else { | |
err = syscall.EINVAL | |
} | |
} | |
return | |
} | |
func makeInheritSa() *syscall.SecurityAttributes { | |
var sa syscall.SecurityAttributes | |
sa.Length = uint32(unsafe.Sizeof(sa)) | |
sa.InheritHandle = 1 | |
return &sa | |
} | |
// Need a custom Open to work with backup_semantics | |
func open(path string, mode int, attrs uint32) (fd syscall.Handle, err error) { | |
if len(path) == 0 { | |
return syscall.InvalidHandle, ERROR_FILE_NOT_FOUND | |
} | |
pathp, err := syscall.UTF16PtrFromString(path) | |
if err != nil { | |
return syscall.InvalidHandle, err | |
} | |
var access uint32 | |
switch mode & (O_RDONLY | O_WRONLY | O_RDWR) { | |
case O_RDONLY: | |
access = GENERIC_READ | |
case O_WRONLY: | |
access = GENERIC_WRITE | |
case O_RDWR: | |
access = GENERIC_READ | GENERIC_WRITE | |
} | |
if mode&O_CREAT != 0 { | |
access |= GENERIC_WRITE | |
} | |
if mode&O_APPEND != 0 { | |
access &^= GENERIC_WRITE | |
access |= FILE_APPEND_DATA | |
} | |
sharemode := uint32(FILE_SHARE_READ | FILE_SHARE_WRITE) | |
var sa *syscall.SecurityAttributes | |
if mode&O_CLOEXEC == 0 { | |
sa = makeInheritSa() | |
} | |
var createmode uint32 | |
switch { | |
case mode&(O_CREAT|O_EXCL) == (O_CREAT | O_EXCL): | |
createmode = CREATE_NEW | |
case mode&(O_CREAT|O_TRUNC) == (O_CREAT | O_TRUNC): | |
createmode = CREATE_ALWAYS | |
case mode&O_CREAT == O_CREAT: | |
createmode = OPEN_ALWAYS | |
case mode&O_TRUNC == O_TRUNC: | |
createmode = TRUNCATE_EXISTING | |
default: | |
createmode = OPEN_EXISTING | |
} | |
h, e := syscall.CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0) | |
return h, e | |
} | |
func getUsnJournalReasonString(reason DWORD) (s string) { | |
var reasons = []string{ | |
"DataOverwrite", // 0x00000001 | |
"DataExtend", // 0x00000002 | |
"DataTruncation", // 0x00000004 | |
"0x00000008", // 0x00000008 | |
"NamedDataOverwrite", // 0x00000010 | |
"NamedDataExtend", // 0x00000020 | |
"NamedDataTruncation", // 0x00000040 | |
"0x00000080", // 0x00000080 | |
"FileCreate", // 0x00000100 | |
"FileDelete", // 0x00000200 | |
"PropertyChange", // 0x00000400 | |
"SecurityChange", // 0x00000800 | |
"RenameOldName", // 0x00001000 | |
"RenameNewName", // 0x00002000 | |
"IndexableChange", // 0x00004000 | |
"BasicInfoChange", // 0x00008000 | |
"HardLinkChange", // 0x00010000 | |
"CompressionChange", // 0x00020000 | |
"EncryptionChange", // 0x00040000 | |
"ObjectIdChange", // 0x00080000 | |
"ReparsePointChange", // 0x00100000 | |
"StreamChange", // 0x00200000 | |
"0x00400000", // 0x00400000 | |
"0x00800000", // 0x00800000 | |
"0x01000000", // 0x01000000 | |
"0x02000000", // 0x02000000 | |
"0x04000000", // 0x04000000 | |
"0x08000000", // 0x08000000 | |
"0x10000000", // 0x10000000 | |
"0x20000000", // 0x20000000 | |
"0x40000000", // 0x40000000 | |
"*Close*", // 0x80000000 | |
} | |
for i := 0; reason != 0; { | |
if reason&1 == 1 { | |
s = s + ", " + reasons[i] | |
} | |
reason >>= 1 | |
i++ | |
} | |
return | |
} | |
// Query usn journal data | |
func queryUsnJournal(fd syscall.Handle) (ujd USN_JOURNAL_DATA, done uint32, err error) { | |
err = DeviceIoControl(fd, FSCTL_QUERY_USN_JOURNAL, []byte{}, &ujd, &done) | |
return | |
} | |
func readUsnJournal(fd syscall.Handle, rujd *READ_USN_JOURNAL_DATA) (data []byte, done uint32, err error) { | |
data = make([]byte, 0x1000) | |
err = DeviceIoControl(fd, FSCTL_READ_USN_JOURNAL, rujd, data, &done) | |
return | |
} | |
func enumUsnData(fd syscall.Handle, med *MFT_ENUM_DATA) (data []byte, done uint32, err error) { | |
data = make([]byte, 0x10000) | |
err = DeviceIoControl(fd, FSCTL_ENUM_USN_DATA, med, data, &done) | |
return | |
} | |
type folderEntry struct { | |
name string | |
parent DWORDLONG | |
} | |
// Build map of folder names using MFT (based on PopulateMethod2) | |
func buildFolderMap() (folders map[DWORDLONG]folderEntry) { | |
folders = make(map[DWORDLONG]folderEntry) | |
drives, _ := walk.DriveNames() | |
fmt.Println(drives) | |
fd, err := open("\\\\.\\C:", syscall.O_RDONLY, FILE_ATTRIBUTE_NORMAL) | |
fmt.Println(fd, err) | |
ujd, _, err := queryUsnJournal(fd) | |
fmt.Printf("ujd = %v\n", ujd) | |
// Open directory to read MFT and store off FRN (file reference numbers) | |
dir, err := open("C:\\", syscall.O_RDONLY, FILE_FLAG_BACKUP_SEMANTICS) | |
fmt.Println("dir,err", dir, err) | |
var fi syscall.ByHandleFileInformation | |
err = syscall.GetFileInformationByHandle(dir, &fi) | |
err = syscall.CloseHandle(dir) | |
fmt.Println("err, fi", err, fi) | |
indexRoot := fi.FileSizeHigh<<32 | fi.FileSizeLow | |
_ = indexRoot | |
med := MFT_ENUM_DATA{0, 0, ujd.NextUsn} | |
for { | |
data, done, err := enumUsnData(fd, &med) | |
if err != nil { | |
fmt.Println(err) | |
} | |
if done == 0 { | |
return | |
} | |
var usn USN = *(*USN)(unsafe.Pointer(&data[0])) | |
// fmt.Println("usn", usn) | |
var ur *USN_RECORD | |
for i := unsafe.Sizeof(usn); i < uintptr(done); i += uintptr(ur.RecordLength) { | |
ur = (*USN_RECORD)(unsafe.Pointer(&data[i])) | |
if ur.FileAttributes&FILE_ATTRIBUTE_DIRECTORY != 0 { | |
nameLength := uintptr(ur.FileNameLength) / unsafe.Sizeof(ur.FileName[0]) | |
fnp := unsafe.Pointer(&data[i+uintptr(ur.FileNameOffset)]) | |
fnUtf := (*[10000]uint16)(fnp)[:nameLength] | |
fn := syscall.UTF16ToString(fnUtf) | |
(*reflect.SliceHeader)(unsafe.Pointer(&fn)).Cap = int(nameLength) | |
// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", fn) | |
folders[ur.FileReferenceNumber] = folderEntry{fn, ur.ParentFileReferenceNumber} | |
} | |
} | |
med.StartFileReferenceNumber = DWORDLONG(usn) | |
} | |
} | |
// Assemble the path by looking up parent pointers in the folders map | |
func getFullPath(folders map[DWORDLONG]folderEntry, parent DWORDLONG) (name string) { | |
for parent != 0 { | |
fe := folders[parent] | |
name = fe.name + "/" + name | |
parent = fe.parent | |
} | |
return | |
} | |
func processAvailableRecords(ch chan *USN_RECORD, folders map[DWORDLONG]folderEntry) { | |
drives, _ := walk.DriveNames() | |
fmt.Println(drives) | |
fd, err := open("\\\\.\\C:", syscall.O_RDONLY, FILE_ATTRIBUTE_NORMAL) | |
fmt.Println("fd, err", fd, err) | |
ujd, _, err := queryUsnJournal(fd) | |
fmt.Printf("ujd = %v\n", ujd) | |
rujd := READ_USN_JOURNAL_DATA{ujd.FirstUsn, 0xFFFFFFFF, 0, 0, 1, ujd.UsnJournalID} | |
for { | |
var usn USN | |
data, done, err := readUsnJournal(fd, &rujd) | |
if err != nil || done <= uint32(unsafe.Sizeof(usn)) { | |
return | |
} | |
usn = *(*USN)(unsafe.Pointer(&data[0])) | |
fmt.Println("usn", usn) | |
var ur *USN_RECORD | |
for i := unsafe.Sizeof(usn); i < uintptr(done); i += uintptr(ur.RecordLength) { | |
ur = (*USN_RECORD)(unsafe.Pointer(&data[i])) | |
if ur.FileAttributes&FILE_ATTRIBUTE_DIRECTORY != 0 { | |
nameLength := uintptr(ur.FileNameLength) / unsafe.Sizeof(ur.FileName[0]) | |
fnp := unsafe.Pointer(&data[i+uintptr(ur.FileNameOffset)]) | |
fn := (*[10000]uint16)(fnp)[:nameLength] | |
(*reflect.SliceHeader)(unsafe.Pointer(&fn)).Cap = int(nameLength) | |
// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", getFullPath(folders, ur.ParentFileReferenceNumber), syscall.UTF16ToString(fn), getUsnJournalReasonString(ur.Reason)) | |
ch <- ur | |
} | |
} | |
rujd.StartUsn = usn | |
if usn == 0 { | |
return | |
} | |
} | |
} | |
// Main | |
func main() { | |
folders := buildFolderMap() | |
ch := make(chan *USN_RECORD) | |
go processAvailableRecords(ch, folders) | |
for { | |
ur := <-ch | |
fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", getFullPath(folders, ur.ParentFileReferenceNumber), getUsnJournalReasonString(ur.Reason)) | |
// fmt.Println(r) | |
} | |
} | |
type PathEvent struct { | |
Path string | |
Flags uint32 | |
Eid uint64 | |
} | |
func WatchPaths(paths []string, eid int64) chan []PathEvent { | |
} | |
func Unwatch(ch chan []PathEvent) { | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment