Problems with "SetupScanFileQueue" callback from SetupAPI C# - c#

I've implemented a SetupAPI wrapper in C# that enumerates devices based on GUIDs. I have, more or less, converted the DevCon c++ example code found on MSDN to C# (Yes I very much know that this is all veyr painfully to do in .NET, but it's fun and a challenge).
I'm able to get all the appropriate information about a certain device, but a problem occurred when I reached the "SetupScanFileQueue" method.
I cant seem to be able to make the "SetupScanFileQueue" to call my callback. The method returns true, so it seems to be working.
My end goal is to get the driver files of the specific device.
Additional information:
The files appear to be added to the FileQueue correctly, I get this popup window that seems to copy the correct files.
// create a file queue so we can look at this queue later
var queueHandler = SetupOpenFileQueue();
if (queueHandler == IntPtr.Zero)
return false;
// modify flags to indicate we're providing our own queue
var deviceInstallParams = new SP_DEVINSTALL_PARAMS();
deviceInstallParams.cbSize = Marshal.SizeOf(deviceInstallParams);
if (!SetupDiGetDeviceInstallParams(handle, ref devInfo, ref deviceInstallParams))
{
error = Marshal.GetLastWin32Error();
return false;
}
// we want to add the files to the file queue, not install them!
deviceInstallParams.FileQueue = queueHandler;
deviceInstallParams.Flags |= DI_NOVCP;
if (!SetupDiGetDeviceInstallParams(handle, ref devInfo, ref deviceInstallParams))
{
error = Marshal.GetLastWin32Error();
return false;
}
// now fill queue with files that are to be installed
// this involves all class/co-installers
if (!SetupDiCallClassInstaller(DIF_INSTALLDEVICEFILES, handle, ref devInfo))
{
error = Marshal.GetLastWin32Error();
return false;
}
// we now have a list of delete/rename/copy files
// iterate the copy queue twice - 1st time to get # of files
// 2nd time to get files
// (WinXP has API to get # of files, but we want this to work
// on Win2k too)
var scanResult = 0;
var count = 0;
var callback = new PSP_FILE_CALLBACK(PSP_FILEFOUND_CALLBACK);
var t = SetupScanFileQueue(queueHandler, SPQ_SCAN_USE_CALLBACK, IntPtr.Zero,
callback, ref count, ref scanResult);
SetupDiDestroyDriverInfoList(handle, ref devInfo, SPDIT_CLASSDRIVER);
if (queueHandler != IntPtr.Zero)
SetupCloseFileQueue(queueHandler);
The definition of my Callback is as follows:
public delegate uint PSP_FILE_CALLBACK(uint context, uint notifaction, IntPtr param1, IntPtr param2);
public static uint PSP_FILEFOUND_CALLBACK(uint context, uint notifaction, IntPtr param1, IntPtr param2)
{
//This callback is never triggered
return 0;
}
Does anyone have any suggestions to what I'm doing wrong in the "SetupScanFileQueue" function, and why the callback is never called?
Any help is very much appreciated!
Edit:
I should also have added the DllImport for the SetupScanFileQueue function:
[DllImport("setupapi.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
private static extern uint SetupScanFileQueue(IntPtr QueueHandle,
int Flags,
IntPtr Window,
PSP_FILE_CALLBACK CallbackRoutine,
int CallbackContext,
out int ScanResult
);
I've also tried it without the CallingConvention.

Related

C# get the full path of a not yet running Process Object

I'm currently trying to accomplish the following:
For an SDK, which we provide to our customers, we want the SDK-developers to be able to provide external application calls, so that they can insert additional buttons. These buttons than will start an external application or open a file with the default application for it (Word for *.docx for example).
There should be some visual distinction between the different buttons, so our approach is to show the icon of the application to be called.
Now, there are three different kind of calls:
(The strings below would always be the value of ProcessStartInfo.FileName)
Calling an application providing the full application path, possibly with environement vars (e.g. "C:\Program Files\Internet Explorer\iexplore.exe" / "%ProgramFiles%\Internet Explorer\iexplore.exe")
Calling an application providing only the executable name, given the application can be found in the PATH Variable (e.g. "iexplore")
Opening a document, without providing an application to open it (e.g. "D:\test.html")
We are looking for a way, to find the appropriate Icon for any given call. For this we have to find the full application path of the application, which will be executed in any of the three ways above, but before we actually have started the Process
Is there a way to find the full path or the icon of a System.Diagnostics.Process or System.Diagnostics.ProcessStartInfo object, before the process has been started?
Important: We must not start the process before (could have side effects)
Example Code:
var process = new Process
{
StartInfo =
{
//applicationPath could be any of the stated above calls
FileName = Environment.ExpandEnvironmentVariables(applicationPath)
}
};
//we have to find the full path here, but MainModule is null as long as the process object has not yet started
var icon = Icon.ExtractAssociatedIcon(process.MainModule.FullPath)
Solution
Thanks to you guys I found my solution. The project linked here at CodeProject provides a solution for my exact problem, which works equally with programs and files and can provide the icon before starting the process. Thanks for the link #wgraham
If you want your UI to be visually-consistent with the rest of the user's machine, you may want to extract the icon from the file using Icon.ExtractAssociatedIcon(string path). This works under the WinForms/GDI world. Alternatively, this question addresses how to complete it with P/Invoke.
Your first two examples shouldn't be too hard to figure out using Environment.ExpandEnvironmentVariables. Your last one is the tougher one - the best bet seems to be using PInvoke to call AssocCreate. Adapted from the pinvoke page (http://www.pinvoke.net/default.aspx/shlwapi/AssocCreate.html):
public static class GetDefaultProgramHelper
{
public unsafe static string GetDefaultProgram(string ext)
{
try
{
object obj;
AssocCreate(
ref CLSID_QueryAssociations,
ref IID_IQueryAssociations,
out obj);
IQueryAssociations qa = (IQueryAssociations)obj;
qa.Init(
ASSOCF.INIT_DEFAULTTOSTAR,
ext, //".doc",
UIntPtr.Zero, IntPtr.Zero);
int size = 0;
qa.GetString(ASSOCF.NOTRUNCATE, ASSOCSTR.COMMAND,
"open", null, ref size);
StringBuilder sb = new StringBuilder(size);
qa.GetString(ASSOCF.NOTRUNCATE, ASSOCSTR.COMMAND,
"open", sb, ref size);
//Console.WriteLine(".doc is opened by : {0}", sb.ToString());
return sb.ToString();
}
catch(Exception e)
{
if((uint)Marshal.GetHRForException(e) == 0x80070483)
//Console.WriteLine("No command line is associated to .doc open verb.");
return null;
else
throw;
}
}
[DllImport("shlwapi.dll")]
extern static int AssocCreate(
ref Guid clsid,
ref Guid riid,
[MarshalAs(UnmanagedType.Interface)] out object ppv);
[Flags]
enum ASSOCF
{
INIT_NOREMAPCLSID = 0x00000001,
INIT_BYEXENAME = 0x00000002,
OPEN_BYEXENAME = 0x00000002,
INIT_DEFAULTTOSTAR = 0x00000004,
INIT_DEFAULTTOFOLDER = 0x00000008,
NOUSERSETTINGS = 0x00000010,
NOTRUNCATE = 0x00000020,
VERIFY = 0x00000040,
REMAPRUNDLL = 0x00000080,
NOFIXUPS = 0x00000100,
IGNOREBASECLASS = 0x00000200,
INIT_IGNOREUNKNOWN = 0x00000400
}
enum ASSOCSTR
{
COMMAND = 1,
EXECUTABLE,
FRIENDLYDOCNAME,
FRIENDLYAPPNAME,
NOOPEN,
SHELLNEWVALUE,
DDECOMMAND,
DDEIFEXEC,
DDEAPPLICATION,
DDETOPIC,
INFOTIP,
QUICKTIP,
TILEINFO,
CONTENTTYPE,
DEFAULTICON,
SHELLEXTENSION
}
enum ASSOCKEY
{
SHELLEXECCLASS = 1,
APP,
CLASS,
BASECLASS
}
enum ASSOCDATA
{
MSIDESCRIPTOR = 1,
NOACTIVATEHANDLER,
QUERYCLASSSTORE,
HASPERUSERASSOC,
EDITFLAGS,
VALUE
}
[Guid("c46ca590-3c3f-11d2-bee6-0000f805ca57"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IQueryAssociations
{
void Init(
[In] ASSOCF flags,
[In, MarshalAs(UnmanagedType.LPWStr)] string pszAssoc,
[In] UIntPtr hkProgid,
[In] IntPtr hwnd);
void GetString(
[In] ASSOCF flags,
[In] ASSOCSTR str,
[In, MarshalAs(UnmanagedType.LPWStr)] string pwszExtra,
[Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pwszOut,
[In, Out] ref int pcchOut);
void GetKey(
[In] ASSOCF flags,
[In] ASSOCKEY str,
[In, MarshalAs(UnmanagedType.LPWStr)] string pwszExtra,
[Out] out UIntPtr phkeyOut);
void GetData(
[In] ASSOCF flags,
[In] ASSOCDATA data,
[In, MarshalAs(UnmanagedType.LPWStr)] string pwszExtra,
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 4)] out byte[] pvOut,
[In, Out] ref int pcbOut);
void GetEnum(); // not used actually
}
static Guid CLSID_QueryAssociations = new Guid("a07034fd-6caa-4954-ac3f-97a27216f98a");
static Guid IID_IQueryAssociations = new Guid("c46ca590-3c3f-11d2-bee6-0000f805ca57");
}
You could call this using string filePathToProgram = GetDefaultProgramHelper.GetDefaultProgram(".docx");
The Process class can do exactly what you want.
Environment Variables like %ProgramFiles% need to be expanded first with Environment.ExpandEnvironmentVariables(string).
1.
using System.IO;
using System.Diagnostics;
string iexplore = #"C:\Program Files\Internet Explorer\iexplore.exe");
string iexploreWithEnvVars = Environment.ExpandEnvironmentVariables(#"%ProgramFiles%\Internet Explorer\iexplore.exe");
2.
public static string FindFileInPath(string name)
{
foreach (string path in Environment.ExpandEnvironmentVariables("%path%").Split(';'))
{
string filename;
if (File.Exists(filename = Path.Combine(path, name)))
{
return filename; // returns the absolute path if the file exists
}
}
return null; // will return null if it didn't find anything
}
3.
Process.Start("D:\\test.html");
You want to put your code into try-catch blocks as well since Process.Start will throw an Exception if the file doesn't exist.
Edit: Updated the code, thanks to Dan Field for pointing out that I missed the meaning of the question. :/

C# ShellExecute on windows 2012r2 incorrect work

I have irritating problem and can't solve it 30+ hours. Please help me because there is a Friday evening and I'd like to go home.
What's a problem?
My service tries to invoke action for file via ShellExecuteA function but action doesn't invoked. When I invoke action from right click menu - this action is done correctly.
Details
I create action for txt and tif extensions:
txt: Action Name: A4, comand line: "C:\Program Files\Windows\NT\Accessories\WORDPAD.EXE" /pt "%1" "Printer" “%3”
tif: Action Name: A4, comand line: "c:\windows\System32\rundll32.exe" "c:\windows\System32\shimgvw.dll",ImageView_PrintTo /pt "%1" "Printer" "%3" "%4"
Where Printer - name of printer.
So, as you can see these commands are configured for printing file. In my service that I wrote on c# I use the next code:
string size = "A4";//name of action for file
int returnCode = Shell32.ShellExecuteAny(0, ref size, ref sourceFilePath, 0, 0, 6);
Where Shell32.ShellExecuteAny is the next:
[System.Security.SuppressUnmanagedCodeSecurity]
public static class Shell32
{
[DllImport("shell32.dll", EntryPoint = "ShellExecuteA", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
extern private static int ShellExecuteA(int hWnd, [MarshalAs(UnmanagedType.VBByRefStr)] ref string lpOperation, [MarshalAs(UnmanagedType.VBByRefStr)] ref string lpFile, IntPtr lpParameters, IntPtr lpDirectory, int nShowCmd);
public static int ShellExecuteAny(int hWnd, ref string lpOperation, ref string lpFile, int lpParameters, int lpDirectory, int nShowCmd)
{
int result;
GCHandle handle = GCHandle.Alloc(lpParameters, GCHandleType.Pinned);
GCHandle handle2 = GCHandle.Alloc(lpDirectory, GCHandleType.Pinned);
try
{
IntPtr tmpPtr = handle.AddrOfPinnedObject();
IntPtr tmpPtr2 = handle2.AddrOfPinnedObject();
result = ShellExecuteA(hWnd, ref lpOperation, ref lpFile, tmpPtr, tmpPtr2, nShowCmd);
}
finally
{
handle.Free();
handle2.Free();
}
return result;
}
}
So, when sometimes my sourceFilePath is txt file, sometimes it's tif file. And this commands work correctly on windows7 and print my files.
But when I try to do so on windows2012r2 I noticed following things. Firstly, my txt files are printed correctly. Secondly, my tif files aren't printed but ShellExecuteAny returns 42(that is means that all have worked correctly). BUT when I right click on tif file and press A4 the file is printed!!!
That's confused me. I don't know what can I do to catch the error or understand what I do wrong.
Thanks.
UPDATE
As Hans says now I use this code:
var startInfo =new ProcessStartInfo(sourceFilePath) {Verb = "A4" };
if(startInfo.Verbs.FirstOrDefault(x=>x == "A4") == null)
{
Logger.Warn("Some warning");
return false;
}
var newProcess = new Process {StartInfo = startInfo};
try
{
newProcess.Start();
}
catch (Exception ex)
{
Logger.Warn("Some message");
}
But the problem is the same. On windows7 all works fine. On windows 2012 no exceptions but for tif files verb doesn't work and pcl files isn't created.
What is a so interesting problem?

Passing array of strings from c# to c++ - memory management

In my code I have c DLL that accepts array of strings:
void Helper::ProcessEvent(PEVENT_RECORD pEvent,wchar_t** OutPutFormattedData)
I'm invoking it with this code:
[DllImport("Helper.dll", EntryPoint = "ProcessEvent")]
internal static extern uint ProcessEvent(
[In, Out]
ref EVENT_RECORD pEvent,
[In, Out]
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)] ref string[] pResult);
In c++ code here is the main code I'm using to fill the array:
for(int i=0;i<list.Size;i++)
{
EventWrapperClass *EWC = new EventWrapperClass();
EWC = list.Events[i];
OutPutFormattedData[i] = new wchar_t [wcslen(HelperFormatMessage(L"%s: %s\n",EWC->GetProperyName(),EWC->GetProperyValue()))+1];
wcscpy(OutPutFormattedData[i] ,HelperFormatMessage(L"%s: %s\n",EWC->GetProperyName(),EWC->GetProperyValue()));
}
And the invoke code:
string[] strArr= new string[1];
NativeHelper.ProcessEvent(ref eventRecord, ref strArr);
I have two questions:
Why when I check the value of the passed array in c# after calling this function I see it's empty (data is exist in c++ code, I debugged it) ?
If I'm allocating memory in c++ dll, where I need to free it? in c++ or c#?
Many thanks!
Edit:
signture of c++:
static __declspec(dllexport) void ProcessEvent(PEVENT_RECORD pEvent, wchar_t** OutPutFormattedData);
If you allocate a (managed or unmanaged) buffer (or array or chars) in C# and then fill in C++:
1. Deallocate in C#.
2. Buffer should be [in] and not have ref or out in C# signature.
If you allocate a buffer (or array of chars) in C++:
1. Pass buffer as [out] out IntPtr.
2. Add deallocate method to C++ signature.
3. Copy buffer into C# buffer or new string
4. Call C++ deallocater from C# with IntPtr.
You can use System.Runtime.InteropServices.Marshal to allocate/deallocate unmanged memory within C#. Do not use to deallocate memory allocated outside .NET!
System.Runtime.InteropServices.Marshal can also be used to copy memory from external buffer (IntPtr) to .NET buffer or string.
Mark imported method as private and wrap with a public (or protected or internal) method that uses the .NET parameters (e.g. that uses System.Runtime.InteropServices.Marshal.Copy and calls external deallocate).
C++:
int ProcessEvent(PEVENT_RECORD eventData, wchar_t*& message)
{
// example code
message = new wchar_t[100];
if (message == 0)
return 1;
}
void DeallocateString(wchar_t* array)
{
delete[] arrayPtr;
}
wchar_t* ErrorCodeToMessage(int errorCode)
{
switch (errorCode)
{
case 0: return 0; // return NULL pointer
case 1: return L"No!!!";
default: return L"WTF!?";
}
}
C#:
[DllImport("Helper.dll", EntryPoint = "ProcessEvent")]
private static extern uint ProcessEventExternal(
[In, Out] ref EventData eventData,
[In, Out, MarshalAs(UnmanagedType.SysInt))] ref IntPtr resultMessages);
[DllImport("Helper.dll", EntryPoint = "DeallocateString")]
private static extern voidDeallocateStringExternal(
[In, MarshalAs(UnmanagedType.SysInt)] IntPtr arrayPtr);
[DllImport("Helper.dll", EntryPoint = "ErrorCodeToMessage")]
private static extern
[return: MarshalAs(UnmanagedType.SysInt)] IntPtr
ErrorCodeToMessageExternal(int errorCode);
public string ProcessEvent(ref EventData eventData)
{
IntPtr resultPtr = IntPtr.Zero;
uint errorCode = ProcessEventExternal(eventData, ref resultPtr);
if (errorCode != null)
{
var errorPtr = ErrorCodeToMessageExternal(errorCode);
// returns constant string - no need to deallocate
var errorMessage = Marshal.PtrToStringUni(errorPtr);
throw new ApplicationException(errorMessage);
}
var result = Marshal.PtrToStringUni(resultPtr);
ExternalDeallocate(resultPtr);
return result;
}
I don't know about question 1, but about question 2:
for(int i=0;i<list.Size;i++)
{
EventWrapperClass *EWC = new EventWrapperClass();
EWC = list.Events[i];
...
}
In the first line, within the loop, you create a new object. In the second line, you assign a different object to your pointer, which is the one stored in list.Events[i].
At this point, nothing points to your first object any longer, and you have a memory leak for every iteration of the loop!
Change the two lines to
EventWrapperClass *EWC = list.Events[i];
as there is no need to create a new object.
Then, if you don't need the event any longer, delete it within the loop and be sure to clear the list afterwards. You may only do this if you are 100% sure the event is not needed any longer. This can easily produce dangling pointers!

C# FMOD Playing stream in realtime

i am trying to play a stream in real time ( I keep appedning data to it as it comes in from a nexternal source) but no matter what FMOD doesn't want to carry on playing after the first chunk that got loaded, it seems as it is copying the memory stream/decoding it before playing, then as it is playing it doesn't use my stream anymore.
I am using the following to play my stream:
var exinfo = new FMOD.CREATESOUNDEXINFO();
exinfo.cbsize = Marshal.SizeOf(exinfo);
exinfo.length = (uint)_buffer.Length;
_result = System.createStream(_buffer, MODE.CREATESTREAM | MODE.OPENMEMORY_POINT , ref exinfo, ref _sound);
FMODErrorCheck(_result);
_result = System.playSound(FMOD.CHANNELINDEX.FREE, _sound, false, ref _channel);
FMODErrorCheck(_result);
But no matter what, it only plays the amount of data that is in the stream at the point of calling playSound.
Can anyone know how to modify the buffer in real time? After the stream has started playing...?
I would recommend you check out the "usercreatedsound" example that ships with FMOD, it should do what you require.
The basic idea is you define the properties of the sound you wish to play in the CreateSoundExInfo structure and provide it with callbacks which you can use to load / stream data from wherever you like.
Function pointer:
private FMOD.SOUND_PCMREADCALLBACK pcmreadcallback = new FMOD.SOUND_PCMREADCALLBACK(PCMREADCALLBACK);
Callback used to populate the FMOD sound:
private static FMOD.RESULT PCMREADCALLBACK(IntPtr soundraw, IntPtr data, uint datalen)
{
unsafe
{
short *stereo16bitbuffer = (short *)data.ToPointer();
// Populate the 'stereo16bitbuffer' with sound data
}
return FMOD_OK;
}
Code to create the sound that will use the callback:
// ...Usual FMOD initialization code here...
FMOD.CREATESOUNDEXINFO exinfo = new FMOD.CREATESOUNDEXINFO();
// You define your required frequency and channels
exinfo.cbsize = Marshal.SizeOf(exinfo);
exinfo.length = frequency * channels * 2 * 5; // *2 for sizeof(short) *5 for 5 seconds
exinfo.numchannels = (int)channels;
exinfo.defaultfrequency = (int)frequency;
exinfo.format = FMOD.SOUND_FORMAT.PCM16;
exinfo.pcmreadcallback = pcmreadcallback;
result = system.createStream((string)null, (FMOD.MODE.DEFAULT | FMOD.MODE.OPENUSER | FMOD.MODE.LOOP_NORMAL), ref exinfo, ref sound);
That should be sufficient to get you going, hope this helps.
If you wish to stream raw data, not PCM data you could achieve this by overriding the FMOD file system. There are two ways to achieve this, the first is by setting the file callbacks in the CreateSoundExInfo structure if this is for one specific file. The second is you can set the file system globally for all FMOD file operations (incase you want to do this with multiple files).
I will explain the latter, it would be trivial to switch to the former though. Refer to the "filecallbacks" FMOD example for a complete example.
Function pointers:
private FMOD.FILE_OPENCALLBACK myopen = new FMOD.FILE_OPENCALLBACK(OPENCALLBACK);
private FMOD.FILE_CLOSECALLBACK myclose = new FMOD.FILE_CLOSECALLBACK(CLOSECALLBACK);
private FMOD.FILE_READCALLBACK myread = new FMOD.FILE_READCALLBACK(READCALLBACK);
private FMOD.FILE_SEEKCALLBACK myseek = new FMOD.FILE_SEEKCALLBACK(SEEKCALLBACK);
Callbacks:
private static FMOD.RESULT OPENCALLBACK([MarshalAs(UnmanagedType.LPWStr)]string name, int unicode, ref uint filesize, ref IntPtr handle, ref IntPtr userdata)
{
// You can ID the file from the name, then do any loading required here
return FMOD.RESULT.OK;
}
private static FMOD.RESULT CLOSECALLBACK(IntPtr handle, IntPtr userdata)
{
// Do any closing required here
return FMOD.RESULT.OK;
}
private static FMOD.RESULT READCALLBACK(IntPtr handle, IntPtr buffer, uint sizebytes, ref uint bytesread, IntPtr userdata)
{
byte[] readbuffer = new byte[sizebytes];
// Populate readbuffer here with raw data
Marshal.Copy(readbuffer, 0, buffer, (int)sizebytes);
return FMOD.RESULT.OK;
}
private static FMOD.RESULT SEEKCALLBACK(IntPtr handle, int pos, IntPtr userdata)
{
// Seek your stream to desired position
return FMOD.RESULT.OK;
}
Implementation:
// Usual init code here...
result = system.setFileSystem(myopen, myclose, myread, myseek, 2048);
ERRCHECK(result);
// Usual create sound code here...

How to move a directory in C# .NET in a single atomic operation

I need to be able to move an entire directory in a single atomic operation, guaranteeing that nothing else on the system will be able to subvert the operation by creating new files after I start, having a lock on a file, etc.
Presumably, I would use System.IO.Directory.Move() if the directories were on the same volume (if Directory.GetDirectoryRoot() is the same), otherwise I'd have to create a new target directory on the other volume and recursively copy/move all the directories and files underneath.
Nothing I've read shows how to gain an exclusive lock to an entire directory leaf in .NET so this can be done safely. Is there a recommended/supported way to do this?
Vista does support transactions in NTFS volumes:
http://msdn.microsoft.com/en-us/magazine/cc163388.aspx
Could you work around this by renaming the "root" directory temporarily (creating a directory with the same name immediately thereafter so that anyone accessing that directory doesn't encounter an error), then work on the files in the renamed directory?
I remember being able to do this at the DOS level by simply renaming the directory. There was a move command, which also seemed to work. But it makes sense. You're not really moving all of the files in the directory, you're just changing the meta data in the directory structure itself. I also remember this from hacking directory structures directly using a disk editor on my fathers Zenith Data Systems 8088. I could make directories invisible by changing the attribute bits on disk, even hiding ".." and "." and making subdirectories appear to be root (the parent directories were invisible). Hope this works for you. I haven't revisited this in hmmm too many years to count ;-). May it work for you.
By the way, you should not have to lock anything because if you're just renaming, it happens really fast, and it's just a single operation.
You can use the Transactional NTFS via PInvoke. Note that it's unclear if it works properly across different volumes, please see the documentation. You may need to use Distributed Transactions, which is significantly more complicated. It will only work on NTFS volumes, not FAT.
Caveat: this code is entirely untested.
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
bool GetVolumeInformationW(
[In, MarshalAs(UnmanagedType.LPWStr)]
string lpRootPathName,
IntPtr lpVolumeNameBuffer,
int nVolumeNameSize,
out int lpVolumeSerialNumber,
out int lpMaximumComponentLength,
out int lpFileSystemFlags,
IntPtr lpFileSystemNameBuffer,
int nFileSystemNameSize
);
[DllImport("KtmW32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
private static extern SafeFileHandle CreateTransaction(
IntPtr lpTransactionAttributes,
IntPtr UOW,
int CreateOptions,
int IsolationLevel,
int IsolationFlags,
int Timeout,
[In, MarshalAs(UnmanagedType.LPWStr)]
string Description
);
[DllImport("KtmW32.dll", SetLastError = true, BestFitMapping = false)]
private static extern bool CommitTransaction(SafeFileHandle hTransaction);
public enum ProgressResponse
{
PROGRESS_CONTINUE, // Continue the copy operation.
PROGRESS_CANCEL, // Cancel the copy operation and delete the destination file.
PROGRESS_STOP, // Stop the copy operation. It can be restarted at a later time.
PROGRESS_QUIET, // Continue the copy operation, but stop invoking CopyProgressRoutine to report progress.
}
public delegate ProgressResponse ProgressRoutine(
long TotalFileSize,
long TotalBytesTransferred,
long StreamSize,
long StreamBytesTransferred,
int dwStreamNumber,
int dwCallbackReason,
IntPtr hSourceFile,
IntPtr hDestinationFile,
IntPtr lpData
);
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
private static extern bool MoveFileTransactedW(
[In, MarshalAs(UnmanagedType.LPWStr)]
string lpExistingFileName,
[In, MarshalAs(UnmanagedType.LPWStr)]
string lpNewFileName,
ProgressRoutine lpProgressRoutine,
IntPtr lpData,
int dwFlags,
SafeFileHandle hTransaction
);
private static bool CheckSupportsTransactions(string filePath)
{
const int FILE_SUPPORTS_TRANSACTIONS = 0x00200000;
if(!GetVolumeInformationW(
Path.GetPathRoot(sourceFullPath),
IntPtr.Zero, 0,
out var _,
out var _,
out var flags,
IntPtr.Zero, 0)
throw new Win32Exception(Marshal.GetLastWin32Error());
return flags & FILE_SUPPORTS_TRANSACTIONS != 0;
}
public static void MoveDirectoryTransacted(string sourceFullPath, string destFullPath, ProgressRoutine progress = null)
{
const int MOVEFILE_COPY_ALLOWED = 0x2;
const int ERROR_REQUEST_ABORTED = 0x4D3;
sourceFullPath = Path.GetFullPath(sourceFullPath);
destFullPath = Path.GetFullPath(destFullPath);
if(!CheckSupportsTransactions(sourceFullPath) ||
!CheckSupportsTransactions(destFullPath))
{
throw new InvalidOperationException("Volume does not support transactions");
}
using (var tran = CreateTransaction(IntPtr.Zero, IntPtr.Zero, 0, 0, 0, 0, null))
{
if (tran.IsInvalid)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (!MoveFileTransactedW(
sourceFullPath,
destFullPath,
progress,
IntPtr.Zero,
MOVEFILE_COPY_ALLOWED,
tran))
{
var error = Marshal.GetLastWin32Error();
if (error == ERROR_REQUEST_ABORTED)
throw new OperationCanceledException();
throw new Win32Exception(error);
}
if (!CommitTransaction(tran))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
}
If you have a CancellationToken, you could invoke it like this
MoveDirectoryTransacted("sourcePath", "destPath",
() => cancelToken.IsCancellationRequested ? ProgressResponse.PROGRESS_CANCEL : ProgressResponse.PROGRESS_CONTINUE);
If you have the ability to run your copy process as "service account" that is only used by the copy process, you could set the permissions of the folder to only allow that account to work with it. Then reset the permissions back to what they were after the copy process finished.
For example, something like the following:
using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
namespace ExclusiveLockFileCopy
{
public class ExclusiveLockMover
{
public DirectorySecurity LockFolder(DirectoryInfo di)
{
var originalSecurity = di.GetAccessControl(System.Security.AccessControl.AccessControlSections.All);
//make sure inherted permissions will come back when UnlockFolder is called
originalSecurity.SetAccessRuleProtection(true, true);
var tmpSecurity = di.GetAccessControl(System.Security.AccessControl.AccessControlSections.All);
// remove all rules
var currentRules = tmpSecurity.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount));
foreach (AccessRule rule in currentRules)
{
tmpSecurity.PurgeAccessRules(rule.IdentityReference);
tmpSecurity.ModifyAccessRule(AccessControlModification.RemoveAll, rule, out var tmpModified);
Console.WriteLine($"Removed access for {rule.IdentityReference.Value}");
}
//add back the current process' identity after the for loop - don't assume the account will show up in the current rule list (i.e. inherited access)
var _me = WindowsIdentity.GetCurrent();
var _meNT = new NTAccount(_me.Name);
tmpSecurity.AddAccessRule(new FileSystemAccessRule(_meNT, FileSystemRights.FullControl, AccessControlType.Allow));
Console.WriteLine($"Ensuring {_meNT.Value} maintains full access");
//strip out inherited permissions
tmpSecurity.SetAccessRuleProtection(true, false);
di.SetAccessControl(tmpSecurity);
//send back the original security incase it is needed later for "unlocking"
return originalSecurity;
}
public void UnlockFolder(DirectoryInfo di, DirectorySecurity originalSecurity)
=> di.SetAccessControl(originalSecurity);
public void CopyFolderExclusive(string srcFolder, string dstFolder)
{
DirectorySecurity diSourceOriginalSecurity = null;
DirectorySecurity diDestinationOriginalSecurity = null;
var diSource = new DirectoryInfo(srcFolder);
var diDestination = new DirectoryInfo(dstFolder);
try
{
diSourceOriginalSecurity = LockFolder(diSource);
if (!diDestination.Exists)
diDestination.Create();
diDestinationOriginalSecurity = LockFolder(diDestination);
// perform your folder/file copy here //
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
finally
{
if (diSourceOriginalSecurity != null)
UnlockFolder(diSource, diSourceOriginalSecurity);
if (diDestinationOriginalSecurity != null)
UnlockFolder(diDestination, diDestinationOriginalSecurity);
}
}
}
}
I'd say what you really need is a transactional file system... which NTFS ain't, and while there have been MS plans for such, it was cut from Longhorn before it became Vista (and from Cairo before that).
You could try to gain exclusive locks on every file in the directory before the move, and do the moving with explicit file reading/writing, but recursively? I'm not so sure that's a good idea... and besides, that won't protect against new files being added.
What are you really trying to do? Why are you worried about concurrent activity?

Categories

Resources