WinAPI, RPC 1702 and FindFirstFileW

April 8, 2009 at 23:39
filed under Dynamics AX
Tagged , , ,

A very common scenario when doing interfaces with other applications, is to have a directory where one application places files, and the other one picks those up and processes them.
Let’s say some application want to interface with Dynamics AX, one way, from that application to AX. That application will drop files of a specific format, say *.txt, in a directory. You’ll want to have a batch job in AX that checks that directory every 5 minutes for those files. When your batch job finds those files, it will open them and do something with that data, then move them to an other directory, so you don’t process them twice.

Now the first thing you’ll probably think of (well, I did…), is to use the class WinAPI. This class has some useful static methods for checking if folders or files exist, finding files with a filename of a certain format. Just the kind of thing we’re looking for. Your code might look like this:

static void loopfilesWinApi(Args _args)
{
    str fileName;
    int handle;
    str format = "*.txt";
    ;

    if(WinApi::folderExists("C:\\Temp\"))
    {
        [handle,fileName] = WinApi::findFirstFile("
C:\\Temp\" + format);
        while(fileName)
        {
            //currentfileName = fileName;
            info(filename);
            filename = WinApi::findNextFile(handle);
        }
        WinApi::closeHandle(handle);
    }
}

That’s perfect. It checks a directory, in this cas C:\temp, for files with the extension txt… until you try to run this in batch. Your batch will have the status error, and there will be an entry in the event log saying:

RPC error: RPC exception 1702 occurred in session xxx

That error message is very misleading, but Florian Dittgen has a nice blog entry about this, that explains why this goes wrong: WinAPI methods a static and run on client. In batch, you can not use these client methods.
But hey! There is a class called WinAPIServer, surely this runs on server, right? Correct, but a lot of the methods available in WinAPI are missing in WinAPIServer. You can however extend the WinAPI server class by copying methods from the WinAPI class and modifying them a bit. Let’s try that.

This is what the WinAPI::FindFirstFile() method looks like:

client static container findFirstFile(str filename)
{
    Binary data = new Binary(592); // size of WIN32_FIND_DATA when sizeof(TCHAR)==2
    DLL _winApiDLL = new DLL(#KernelDLL);
    DLLFunction _findFirstFile = new DLLFunction(_winApiDLL, 'FindFirstFileW');

    _findFirstFile.returns(ExtTypes::DWord);

    _findFirstFile.arg(ExtTypes::WString,ExtTypes::Pointer);

    return [_findFirstFile.call(filename, data),data.wString(#offset44)];
}

As you can see, this method uses the function FindFirstFileW from the Kernel32 dll located in the folder C:\WINDOWS\system32. Let’s copy this method to WinAPIServer and modify it to look like this:

server static container findFirstFile(str filename)
{
    #define.KernelDLL('KERNEL32')
    #define.offset44(44)

    InteropPermission interopPerm;
    Binary data;
    DLL _winApiDLL;
    DLLFunction _findFirstFile;
    ;

    interopPerm = new InteropPermission(InteropKind::DllInterop);
    interopPerm.assert();

    data = new Binary(592); // size of WIN32_FIND_DATA when sizeof(TCHAR)==2
    _winApiDLL = new DLL(#KernelDLL);
    _findFirstFile = new DLLFunction(_winApiDLL, 'FindFirstFileW');

    _findFirstFile.returns(ExtTypes::DWord);

    _findFirstFile.arg(ExtTypes::WString,ExtTypes::Pointer);

    return [_findFirstFile.call(filename, data),data.wString(#offset44)];
}

Three things have changed here:

  1. “Client” has been change to “Server” so it can run on server;
  2. The code has been changed to use the InteropPermission class; this is required because the code runs on server;
  3. Initialization of the variables is moved to after assertion of the InteropPermission.

When you test this in batch (running on server), this will work, and you can do this for all WinAPI methods in the examples above… until you try this on a x64 architecture. You’ll receive a message saying that an error occurred when calling the FindFirstFileW function in the Kernel32 DLL (the exact message slipped my mind).
The problem is described on the Axapta Programming Newsgroup. You can code your way around this problem, but I think the message is clear: don’t use WinAPI.

Instead, use the .NET Framework System.IO namespace (watch out for lowercase/uppercase here), in our case in specific, System.IO.File and System.IO.Directory.
I get this nice code example from Greg Pierce’s blog (modified it a bit):

static void loopfilesSystemIO(Args _args)
{
    // loop all files that fit the required format
    str fileName;
    InteropPermission interopPerm;

    System.Array files;
    int i, j;
    container fList;
    ;

    interopPerm = new InteropPermission(InteropKind::ClrInterop);
    interopPerm.assert();

    files = System.IO.Directory::GetFiles(@"C:\temp", "*.txt");
    for( i=0; i<ClrInterop::getAnyTypeForObject(files.get_Length()); i++ )
    {
        fList = conins(fList, conlen(fList)+1, ClrInterop::getAnyTypeForObject(files.GetValue(i)));
    }

    for(j=1;j&lt;= conlen(fList); j++)
    {
        fileName = conpeek(fList, j);
        info(fileName);
    }
}

This will run on client and on server without any problems. To have clean code, you could create your own class, just like WinAPI, but using the System.IO namespace. That way, you can reuse this code and save time.

Conclusion: abandon WinAPI, and use System.IO instead.

46 comments

RSS / trackback

  1. Jeroen Doens

    Nice research!

  2. Amber

    How do you then move them into another directory to avoid processing the same files the next time the batch runs?

    Thanks!

  3. Klaas Deforche

    You can use this method:

    server static void moveFile(str fileName, str newFileName)
    {
        #File
        Set                 permissionSet;
        ;

        permissionSet =  new Set(Types::Class);
        permissionSet.add(new FileIOPermission(fileName,#io_write));
        permissionSet.add(new InteropPermission(InteropKind::ClrInterop));

        CodeAccessPermission::assertMultiple(permissionSet);

        System.IO.File::Move(fileName, newFileName);

        CodeAccessPermission::revertAssert();
    }

    Let me know if it works or if you need more help.

  4. Rob

    Hi there!

    I’m creating an interface between Dax and another app just the way described by using textfiles in a folder. Everything works fine, only I have one problem: the exportscript from app1 (Oracle app) and the DAX importscript are executed asynchronic. Things go wrong when app 1 hasn’t finished writing the exportfile and Dax already starts importing the file.

    I there a way to determine the filesize? So I can import only when the filesize > 0 kb. I tried WinAPI::filesize but it always returns 0.

    Thanx!

    Rob

  5. Klaas Deforche

    Hi Rob,

    I’m familiar with the problem, but it’s a tricky one.
    If you are looking to determine the file size, this should return the file size in bytes:

    info(strfmt("%1", WinAPI::filesize(@"C:\testing.txt")));

    I don’t know if that’s a good way to check if the process has finished writing the file, because the size could be greater than 0, even if the process hasn’t finished writing (for example when it is appending information to the file).

    Here’s a few alternative ways to deal with this:
    1. The Oracle app create the file in a temporary directory, and when it’s finished, it moves the file to the directory you are monitoring in AX.

    2. Or the Oracle app creates the file with a filename or extension you are not monitoring, and renames it to the correct file name when it’s finished (my favourite).

    3. Or the Oracle app create a second file when it has finished writing, with the same filename, but a different extension (eg: filename.csv and filename.done), and you only read files that have a corresponding .done file (check this in AX).

    Hope this helps!

  6. Michael

    When using the code in #3 above I get the following error:

    ClrObject static method invocation error.

    Anyone have ideas on what to check as to the cause of this error?

  7. Klaas Deforche

    Hi Michael,

    I’ve had this problem as well, especially when executing code in batch.

    It probably has something to do with escaping the slashes in your string.
    Try this:

    System.IO.File::Move(strfmt(@"%1", fileName), strfmt(@"%1", newFileName));

    Also, using FileName as datatype for you parameters in stead of str might do the trick.
    Let us know what works for you.

  8. Michael

    Thanks for the quick response. I ended up adding a try/catch to pull in the modt recent message and it told me that the file was still in use which pointed me to the fact that I had not closed the file (i.e. set it to NULL) before trying to move it. So once I did that it worked fine.

    Thanks so much for this blog post – it saved me huge amounts of time.

  9. Jan VO

    Thanks for the tips regarding escaping slashes, it works .

  10. John

    I am using System.IO.File::Exists method to check a file and if it exists create a new file with a different name. Problem is when i call this code using a batch server getting error:

    permissionSet = new Set(Types::Class);
    permissionSet.add(new FileIOPermission(successDirectory + filename, #io_read));
    CodeAccessPermission::assertMultiple(permissionSet);

    Request for the permission of type ‘InteropPermission’ failed.
    (S)\Classes\InteropPermission\demand
    (S)\Classes\CLRInterop\staticInvoke
    (S)\Classes\SIG_AIInboundController\run – line 190
    (S)\Classes\BatchRun\runJobStatic – line 62

    Can you think of any idea why this is happening

    //w_OrigfilenameFilename = filename;
    if (!System.IO.File::Exists(successDirectory + filename))

  11. Klaas Deforche

    Hi John,

    I believe the problem is that you didn’t add the InteropPermission to your set of permissions.

    Please add the following line (before CodeAccessPermission::assertMultiple()) and try again:

    permissionSet.add(new InteropPermission(InteropKind::ClrInterop));

    You need the ClrInterop permission to use this function:
    System.IO.File::Exists().

    Hope this helps.

  12. Hike

    Hi Klaas,

    Our system is based on the x64 architecture and I encounter the same problem while downloading the file via FTP. It seems that the WinInet class also cannot work on the x64 architecture in batch. Do you have any experience with this case?

    Thanks,
    Hike

  13. Klaas Deforche

    Hi Hike,

    Sorry, no specific experience with FTP on x64.
    I will look into it though, maybe I can find a solution.

    The suggestion by Vladi on the newsgroups could help you: http://www.microsoft.com/communities/newsgroups/en-us/default.aspx?dg=microsoft.public.axapta.programming&tid=c5b2f7c6-0dc3-49f3-a49e-dcd836625563&cat=&lang=&cr=&sloc=&p=1.
    It’s about the Wow64DisableWow64FsRedirection and
    Wow64RevertWow64FsRedirection functions.
    More info on msdn: http://msdn.microsoft.com/en-us/library/aa365743%28VS.85%29.aspx
    I haven’t tried it myself, but that just might do it.

    Best regards,
    Klaas.

  14. Ajit

    Hi,

    I have used “findFirstFile” method written by you for server but I am getting below error. Please help!

    “Function ‘FindFirstFileW’ in DLL library ‘KERNEL32’ has caused an exception.”

    Regards,
    Ajit

  15. Klaas Deforche

    Hi Ajit,

    That’s right, you’re AOS probably has a x64 architecture?
    I would advise using the last method (job named loopfilesSystemIO), that will certainly work.

    Regards,
    Klaas.

  16. Ajit

    Yes Klaas. Its 64 bit server. I’ll try loopfilesSystemIO.
    Thanks.

  17. Matic

    Hey.
    Nicely written!

    I have similar problem, the difference is that I need a list of directories:

    permissionSet = new Set(Types::Class);
    permissionSet.add(new FileIOPermission(filePath, ‘r’));
    permissionSet.add(new InteropPermission(InteropKind::ClrInterop));
    CodeAccessPermission::assertMultiple(permissionSet);

    files = System.IO.Directory::GetDirectories(filePath);

    This last line still throws “ClrObject static method invocation error.”
    Variable filePath holds some network path, although I am sure server has read/write access.
    This code runs in a batch job.

    Any suggestions?

    Thanks,
    Matic

  18. Klaas Deforche

    Hi Matic,

    I tested you code like this and it ran fine:

    static void klForGetDirs(Args _args)
    {
        Set permissionSet;
        System.Array files;
        int i, j;
        container fList;
        str filename;
        ;
       
        permissionSet = new Set(Types::Class);
        permissionSet.add(new FileIOPermission(@"C:", "r"));
        permissionSet.add(new InteropPermission(InteropKind::ClrInterop));
        CodeAccessPermission::assertMultiple(permissionSet);
     
        files = System.IO.Directory::GetDirectories(@"C:");

         for( i=0; i<ClrInterop::getAnyTypeForObject(files.get_Length()); i++ )
         {
             fList = conins(fList, conlen(fList)+1, ClrInterop::getAnyTypeForObject(files.GetValue(i)));
         }

        for(j=1;j<= conlen(fList); j++)
        {
             fileName = conpeek(fList, j);
             info(fileName);
        }
    }

    I think the error “ClrObject static method invocation error.” indicates that the parameter you provide to the function is incorrect. Try a fixed path ‘like @”C:\” to see if that works. Then you know that is (or isn’t) the problem. As I’ve said in previous comments, AX behaves a bit strange when it come to filepaths (and escaping those slashes), I have trouble figuring that out too :).

  19. Matic

    It’s kinda strange.
    You are right, @”C:\” works fine.

    But for example @”\\serverName\Share” does not.
    In windows explorer I can access those shares and share permissions are set for Everyone.

    It’s clearly a network permissions problem.

    I’ll do some more research and keep you posted.

  20. Anonymous

    I am having this warning being logged in the Event Viewer. Does this problem may cause stability issues in the server? Because after a number of entries, it goes down unexpectedly …

    Any help will be appreciated.

  21. Ajit

    Hi Klass,

    I am using below code for moving file from one location to another location. Location defined on parameters

    In Location – \\ServerName\FolderName
    Out Location – \\ServerName\Out Folder Name

    But I am getting – ClrObject static method invocation error.
    I am ruuning code on server. Any help will be appreciated.
    Tell me if I am doing something wrong here.

    permissionSet.add(new FileIOPermission(fileName,#io_read));
    permissionSet.add(new InteropPermission(InteropKind::ClrInterop));

    [path, filenameL,fileExt] = fileNameSplit(filename);
    fileNameTo = CustParameters::find().CSVArchieveLocation +’\\’+ fileNameL + ‘_’ +strrem(strrem(date2str(SystemDateGet(),213,2,1,2,1,4),’//’),’ ‘) + ‘_’ + time2str(timenow(),3,3)+’.csv’;

    permissionSet.add(new FileIOPermission(fileNameTo,#io_Write));
    CodeAccessPermission::assertMultiple(permissionSet);
    System.IO.File::Move(fileName,fileNameTo);
    CodeAccessPermission::revertAssert();

    Thanks,
    Ajit

  22. Klaas Deforche

    Hi Ajit,

    Can you try the same as Micheal (comment 8), a try/catch to capture the .net exception.
    This might point you to the problem.

    This could help:
    http://axatluegisdorf.blogspot.com/2010/05/decode-clr-exception-message.html
    And:
    http://msdn.microsoft.com/en-us/library/ee677495.aspx

  23. Leandrolopesp

    Oh man! You saved my day :)

  24. Klaas Deforche

    Leandrolopesp, glad it helped :)

  25. Ajit

    I want to delete files using WINAPIServer in AX 2012. When I change class run on type to “Client” then its is working but when I am changing to server, it doesn’t work. Any pointer would be helpful.

    Thanks,
    Ajit

  26. Klaas Deforche

    Hi Ajit,

    What is the error you’re getting?
    Are you using a local path (C:\…) or UNC path (\\server\…)?
    Is it possible to copy your code here?

  27. Ajit

    Thanks Klaas for your reply. Issue has been resolved. There was an issue with AOS service account.

  28. Vince

    Hi Klaas,

    I’m using AX4.0 to get a number of files spread out in several directories.

    My code:

    files = System.IO.Directory::GetFiles(@”C:\temp”, “*.txt”, System.IO.SearchOption::AllDirectories);

    AX throws a syntax error when using System.IO.SearchOption::AllDirectories.
    If I don’t use it, no error is given, but the result is incomplete of course.

  29. Klaas Deforche

    Hi Vince,

    It appears that enumerations are not supported in AX 4.0.
    I got my info here: http://www.axaptapedia.com/.NET_Integration#Enumerations
    They propose a workaround, but it’s probably more trouble than it’s worth.

    What you could try is also loop your directories and recursive get all files that way.

    // pseudocode
    array getfiles(dir _dir)
    {
    array += getallfiles();

    while locdir in _dir
    array += getfiles(locdir);

    return array;
    }

  30. Muhammad Hussain Bandukda

    I am facing an issue while moving a text file from a network path. There is no error but I dont see the file copied. Here is the code:

    static void Job16(Args _args)
    {
    #File
    Set permissionSet;
    ;

    permissionSet = new Set(Types::Class);
    permissionSet.add(new FileIOPermission(“\\\\Ax-uat\\rms\\IA\101_20120228_IA.txt”,#io_write));
    permissionSet.add(new InteropPermission(InteropKind::ClrInterop));

    CodeAccessPermission::assertMultiple(permissionSet);
    System.IO.File::Move(strfmt(@”%1″, “\\\\Ax-uat\\rms\\IA\101_20120228_IA.txt”), strfmt(@”%1″, “\\\\AX-UAT\\RMS_Backup\\IA\\archieve\101_20120228_IA.txt”));

    CodeAccessPermission::revertAssert();
    }

    is there something that I am missing here?

  31. Deepak

    Hi all,

    In AX, I am tring to send mail with attachments. In that I need to get the size of the file which is attached in mail.
    Because the original file size and attached file size is different.
    Can any one help me!

  32. Klaas Deforche

    Hi Deepak,

    I have no experience with that, sorry.
    You will probably get an answer quicker if you post your question on the Dynamics AX forums: http://community.dynamics.com/product/ax/f/33.aspx

  33. dwojt

    Ajit,

    could you explain your issue with AOS service account?

    Thanks,
    dwojt

  34. Ben

    @Vince, Klaas Deforche
    I don’t know if this is still of interest for you, but actually you can use .NET-Enumerations in Dynamics AX 4.0. You have to use the System.Enum.Parse-method to accomplish this task:

    System.IO.SearchOption searchOption;
    str folder = ”;
    str pattern = ‘*.*’;
    ;
    searchOption =System.Enum::Parse(System.Type::GetType(“System.IO.SearchOption”),”AllDirectories”);
    files = System.IO.Directory::GetFiles(folder,pattern,searchOption);

    Many Greets, Ben

  35. Shiv Prateek

    Hi Klass,

    Is there any way to open any excel file without Using SysExcelApplication Class. These classes only run on client machine. But If we want to open excel file by a batch job, which runs on server, it gives me an error message “The impersonated(runAs) tries to invoke a method which is only available for client side”. Do you have any solution or a way to open excel by using methods which also run on server side.

  36. Klaas Deforche

    Hi Shiv,

    Yeah, that’s quit annoying.
    You could use OpenXML, although I do not have experience with it.
    Johan Van Veldhuizen has a blog post about it: http://www.van-veldhuizen.eu/blog/2012/10/06/import-data-using-ms-excel-into-dynamics-ax-2012/.

    Hope this helps.

    Update: when I can, I try not to use Excel because my experience is that it’s slow. I try to use csv files where I can. Maybe that is an option for you too. Users can edit csv file in excel too, they just have to make sure they save it correctly.

  37. Shiv Prateek

    Hi Klass,

    Thanks for your response. Would this OpenXML dll work for AX 2009 ? Or It would only work in Ax 2012. I am actually working on Ax 2009.

    Regards,
    Shiv

  38. Arun joseph

    How to move the file from one folder to other folder through batch processing

  39. Arun joseph

    Hi All,

    How to move the file from one folder to other folder through batch processing.

    Please help me ..

    Regards

    Arun joseph

  40. Klaas Deforche

    Hi Arun,

    I’m sorry for the very late reply, your message got lost between spam.
    You can do so using System.IO.File::Move(). Don’t forget to assert the appropriate permissions as demonstrated above.

  41. Shiv Prateek

    Hi All,

    Can I use Binary class to convert decimal value to Binary Value or reverse. If yes, Then how?

    Thanks,

  42. Klaas Deforche

    Hi Shiv,

    I don’t know of any way to convert to binary. In a way you do not really have to. Binary is just a way to represent a number, for example an integer. If you “info” a number in AX, it will always be shown in decimal, not binary or hex or something. But you can use integers to store binary numbers (only you have to use decimal).
    But anyway, if you want to know the bit representation, you can write your own function using binary operators, especially the & operation (see: http://msdn.microsoft.com/en-us/library/aa870833.aspx).

    For example for the binary representation of 6:
    6 & 1 = 0
    6 & 2 = 1
    6 & 4 = 1
    -> so 6 binary is 110.

    A bit off topic but I hope it helps :).

  43. Ax beginner

    Hi Ajit

    As per the point 21) I am also facing the same error. I want to move a file from one folder to another folder. Both my folders are from the network and I am unable to move the file. This happens only when I run it on the server side. If I run it on client I am not facing this error.
    Also as per the point 27) you said that the Issue has been resolved. There was an issue with AOS service account. Could you please clarify how u resolved it on the server side.

    Any help will be appreciated.

  44. Rizky

    Hi All,

    How get file in client? I code in server,I don’t get file.
    This is code :

    server static boolean SendDataLive(SM_IntegrationSOA _SM_IntegrationSOA)
    {
    InteropPermission permission;
    liveupload.file.hrm.es_erp_dokumen_uploadClient _WS;
    liveupload.file.hrm.file_attachmentsType _fileAttach;
    liveupload.file.hrm.attachmentsType _attachtype;
    liveupload.file.hrm.RESULTType _resulttype;

    System.Byte[] byteArray;
    System.Exception ex;
    HistoryDokumenPegawai HistoryDokumenPegawai;
    ;

    permission = new InteropPermission(InteropKind::ClrInterop);
    permission.assert();
    try
    {
    _fileAttach = new liveupload.file.hrm.file_attachmentsType();
    _attachtype = new liveupload.file.hrm.attachmentsType();
    _WS = new liveupload.file.hrm.es_erp_dokumen_uploadClient();
    _resulttype = new liveupload.file.hrm.RESULTType();

    byteArray = System.IO.File::ReadAllBytes(_SM_IntegrationSOA.FileName);
    //SM_IntegrationSOA::FileClient(_SM_IntegrationSOA.filename);//
    _attachtype.set_DESKRISPI(“”);
    _attachtype.set_TIPE_DOKUMEN(_SM_IntegrationSOA.IdTipeDokumenDetail);
    _attachtype.set_NAMA_FILE(_SM_IntegrationSOA.FileName);
    _attachtype.set_VERSI(System.Convert::ToString(_SM_IntegrationSOA.Versi));
    _attachtype.set_ATTACHMENT(byteArray);
    _fileAttach.set_attachments(_attachtype);
    _resulttype = _WS.es_erp_dokumen_upload(_SM_IntegrationSOA.EmplId,_fileAttach);

    HistoryDokumenPegawai.EmplId = _SM_IntegrationSOA.EmplId;
    HistoryDokumenPegawai.IdTipeDokumen = _SM_IntegrationSOA.IdTipeDokumen;
    HistoryDokumenPegawai.IdTipeDokumenDetail = _SM_IntegrationSOA.IdTipeDokumenDetail;
    HistoryDokumenPegawai.Versi = _SM_IntegrationSOA.Versi;
    HistoryDokumenPegawai.DateUpload = systemdateget();
    HistoryDokumenPegawai.StatusUpload = System.Convert::ToString(_resulttype.get_Description());
    HistoryDokumenPegawai.FileDokumen = _SM_IntegrationSOA.FileName;
    HistoryDokumenPegawai.insert();

    }
    catch(Exception::CLRError)
    {
    ex = ClrInterop::getLastException();
    info(ex.ToString());
    }

    if(ex)
    return false;
    else
    return true;

    }

    Any help will be appreciated.

  45. Mohamed Shuhaib

    HI
    I have installed and configured production floor app .
    I am getting the same error .
    which code needs to be changed ?
    Could you please guide me

  46. Klaas Deforche

    Hi Mohamed,

    I am not familiar with the production floor app… You can start by scanning your code for code that only works on client (things with forms, dialogs, winapi, Excel,…). You can also try debugging using visual studio on the aos or using the tracing cockpit to figure out where the error is thrown.

respond