Search Unity

External file access on WSA IL2CPP build

Discussion in 'Windows' started by smenyhart, Apr 17, 2019.

  1. smenyhart

    smenyhart

    Joined:
    May 6, 2016
    Posts:
    35
    When the app is registered to handle a file extension and a file with that extension is opened the WSA build will be notified of the file in the following auto generated code found in App.xaml.cpp:

    Code (CSharp):
    1.  
    2. void App::OnFileActivated(FileActivatedEventArgs^ args)
    3. {
    4.     String^ appArgs = "File=";
    5.  
    6.     m_SplashScreen = args->SplashScreen;
    7.  
    8.     bool firstFileAdded = false;
    9.  
    10.     for (auto file : args->Files)
    11.     {
    12.         if (firstFileAdded)
    13.         {
    14.             appArgs += ";";
    15.         }
    16.         else
    17.         {
    18.             firstFileAdded = true;
    19.         }
    20.  
    21.         appArgs += file->Path;
    22.     }
    23.  
    24.     InitializeUnity(appArgs);
    25. }
    26.  
    I can then get access to this file path from within Unity using the following code:
    Code (CSharp):
    1.  
    2. public class HoloLensExternalDataInput : MonoBehaviour
    3. {
    4.     // Use this for initialization
    5.     void Start ()
    6.     {
    7.         UnityEngine.WSA.Application.windowActivated += Application_windowActivated;
    8.     }
    9.  
    10.     private void Application_windowActivated(UnityEngine.WSA.WindowActivationState state)
    11.     {
    12.         if (state != UnityEngine.WSA.WindowActivationState.Deactivated)
    13.         {
    14.             var arguments = UnityEngine.WSA.Application.arguments;
    15.             Debug.Log("Application_windowActivated - arguments: " + arguments);
    16.  
    17.             const string FILE_ARG = "File=";
    18.             const string UIR_ARG = "Uri=";
    19.  
    20.             if (arguments.StartsWith(FILE_ARG))
    21.             {
    22.                 var filesStr = arguments.Substring(FILE_ARG.Length);
    23.                 var files = filesStr.Split(';');
    24.  
    25.                 if (files.Length > 0)
    26.                 {
    27.                     Debug.Log("External File: " + files[0]);
    28. #if ENABLE_WINMD_SUPPORT
    29.                     var storeageFile = System.Threading.Tasks.Task.Run(async () => { return await Windows.Storage.StorageFile.GetFileFromPathAsync(files[0]); }).Result;
    30.                     Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.Add(storeageFile);
    31. #endif
    32.  
    33.                     DataLoadManager.LoadExternalFile(files[0]);
    34.                 }
    35.             }
    36.             else if (arguments.StartsWith(UIR_ARG))
    37.             {
    38.                 var uri = arguments.Substring(UIR_ARG.Length);
    39.                 //TODO: handel URI
    40.             }
    41.         }
    42.     }
    43. }
    44.  
    The problem is 2 fold.

    First, I shouldn't have to call "Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.Add" myself. The auto generated code should do this, otherwise the file path passed into Unity is useless and cannot be accessed.

    The second problem is that now that I have to compile with IL2CPP instead of the .NET backend because I switched to Unity 2019, this workaround no longer works. One of the two lines after "#if ENABLE_WINMD_SUPPORT" (my guess is the task runner) crashes with the following exception:

    UnauthorizedAccessException: Attempted to perform an unauthorized operation.
    at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x00000] in <00000000000000000000000000000000>:0
    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x00000] in <00000000000000000000000000000000>:0
    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00000] in <00000000000000000000000000000000>:0
    at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00000] in <00000000000000000000000000000000>:0
    at System.Runtime.CompilerServices.StrongBox`1[T]..ctor (T value) [0x00000] in <00000000000000000000000000000000>:0
    at EWIHARParser.PropertyValue+<GetEnumerator>d__8..ctor (System.Int32 <>1__state) [0x00000] in <00000000000000000000000000000000>:0
    at System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner.InvokeMoveNext (System.Object stateMachine) [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.ContextCallback.Invoke (System.Object state) [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
    at System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner.Run () [0x00000] in <00000000000000000000000000000000>:0
    at System.Action.Invoke () [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction (System.Action action, System.Boolean allowInlining, System.Threading.Tasks.Task& currentTask) [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.Task.FinishContinuations () [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.Task.FinishStageThree () [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.Task.FinishStageTwo () [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.Task.Finish (System.Boolean bUserDelegateExecuted) [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.Task`1[TResult].TrySetException (System.Object exceptionObject) [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.TaskCompletionSource`1[TResult].TrySetException (System.Exception exception) [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.AsyncInfoToTaskBridge`1[TResult].Complete (Windows.Foundation.IAsyncInfo asyncInfo, System.Func`2[T,TResult] getResultsFunction, Windows.Foundation.AsyncStatus asyncStatus) [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.AsyncInfoToTaskBridge`1[TResult].CompleteFromAsyncOperation (Windows.Foundation.IAsyncOperation`1[TResult] asyncInfo, Windows.Foundation.AsyncStatus asyncStatus) [0x00000] in <00000000000000000000000000000000>:0
    at Windows.Foundation.AsyncOperationCompletedHandler`1[TResult].Invoke (Windows.Foundation.IAsyncOperation`1[TResult] asyncInfo, Windows.Foundation.AsyncStatus asyncStatus) [0x00000] in <00000000000000000000000000000000>:0
    Rethrow as AggregateException: One or more errors occurred.
    at System.Threading.Tasks.Task.ThrowIfExceptional (System.Boolean includeTaskCanceledExceptions) [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.Task`1[TResult].GetResultCore (System.Boolean waitCompletionNotification) [0x00000] in <00000000000000000000000000000000>:0
    at System.Threading.Tasks.Task`1[TResult].get_Result () [0x00000] in <00000000000000000000000000000000>:0
    at HoloLensExternalDataInput.Application_windowActivated (UnityEngine.WSA.WindowActivationState state) [0x00000] in <00000000000000000000000000000000>:0
    at UnityEngine.WSA.WindowActivated.Invoke (UnityEngine.WSA.WindowActivationState state) [0x00000] in <00000000000000000000000000000000>:0
    at UnityEngine.WSA.Application.InvokeWindowActivatedEvent (UnityEngine.WSA.WindowActivationState state) [0x00000] in <00000000000000000000000000000000>:0

    My preferred fix would be for Unity to add the call to "Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.Add" to the auto generated code in App.xaml.cpp. But until that is done can someone help me figure out a workaround?
     
  2. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    6,555
    Isn't FutureAccessList only needed if you want to access that file in subsequent app launches?

    Note that if you modify App.xaml.cpp and add FutureAccessList code there, Unity will not overwrite it when you build your project on top of it. It was designed this way so you could put those files into your source control and make any changes you want.

    As for the exception - I suppose it didn't use to happen on .NET scripting backend?
     
  3. smenyhart

    smenyhart

    Joined:
    May 6, 2016
    Posts:
    35
    That's correct. The exception didn't happen with the .NET scripting backend. As for using the file without adding it to the FutureAccessList, this used to work on a previous version of the HoloLens OS, but once I updated it to the latest a few months ago it stopped working. I could no longer access that file path passed into Unity with regular .NET file IO classes, without first adding it to the FutureAccessList.

    I will try modifying App.xaml.cpp directly to work around this issue.
     
  4. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    6,555
    Do you mind filling a bug report for this? If it worked on .NET but doesn't work on IL2CPP, we consider it a pretty high priority issue.
     
  5. smenyhart

    smenyhart

    Joined:
    May 6, 2016
    Posts:
    35
    Ok I'll file a bug report. It appears calling any async task based code causes it to crash.

    I also tried adding the call to FutureAccessList directly to App.xaml.cpp, and that compiles and runs without any errors, but it no longer fixes the problem. Even after adding the file path to the FutureAccessList, it remains inaccessible. Calling File.Copy on it produces a FileNotFound exception. Calling File.SetAttributes produces a security exception. Passing the path to a native code library that tries to read it also fails. I have not been able to find a workaround for this.
     
  6. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    6,555
    How does the native code try to read it? What is the actual path to the file? Does it work if you try to access the file using StorageFile instead of System.IO APIs from C#?
     
  7. smenyhart

    smenyhart

    Joined:
    May 6, 2016
    Posts:
    35
    [XamlViewManager] SwitchToView OnCompleted callback status : 1
    Application_windowActivated - arguments: File=C:\Data\Users\steve\Downloads\Nanobox Assembly.plmx

    Exception thrown at 0x7688F322 in JTViewer.exe: Microsoft C++ exception: Il2CppExceptionWrapper at memory location 0x07BEECD4.
    FileNotFoundException: C:\Data\Users\steve\Downloads\Nanobox Assembly.plmx does not exist
    at System.IO.File.Copy (System.String sourceFileName, System.String destFileName, System.Boolean overwrite) [0x00000] in <00000000000000000000000000000000>:0
    at System.IO.File.Copy (System.String sourceFileName, System.String destFileName) [0x00000] in <00000000000000000000000000000000>:0
    at DataLoadManager.Update () [0x00000] in <00000000000000000000000000000000>:0

    I believe the native code just uses a C++ file stream.

    Both the System.IO and native code file access used to work with the .NET backend after the file path was added to the FutureAccessList. (It also used to work without being added to FutureAccessList on a previous older version of the HoloLens OS)

    I have not been able to get code that uses StorageFile to work because it produces an exception while trying to run any async Threading.Tasks code using IL2CPP.
     
  8. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    6,555
    Instead of using Task.Run(() => ...).Result, you should be able to just await the storage operations directly without Task.Run. Does that not work?

    I'll be looking at the IL2CPP file operations. I don't have the solution right now, but I hope to have one soon.
     
  9. smenyhart

    smenyhart

    Joined:
    May 6, 2016
    Posts:
    35
    I believe that using the await keyword just uses the same taskrunner code under the hood, but don't quote me on that. I did try some C# code using await file operations and it produced the same kind of task running exception above when ran on IL2CPP.
     
  10. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    6,555
    That doesn't sound right, but I can dig in. Can you by any chance make a repro project that showcases this issue? I tried using StorageFile on my end and things seem to work. It could be you're hitting some edge case with the file picker or something.
     
  11. smenyhart

    smenyhart

    Joined:
    May 6, 2016
    Posts:
    35
    I'll try making a sample app that reproduces the issue. Some differences that you may not be reproducing might be.
    1) I'm running on a HoloLens
    2) I'm not using a file picker. I associate the app with a certain file extension and the OS opens my app when I try to open a file with that extension.
    3) I need to access the file slightly later than within the event that notifies me the file has been picked. So I store the file path and access it on the next Update cycle.

    Have you at least been able to confirm that the file can't be accessed using regular .NET IO classes? And shouldn't it be accessible from regular .NET IO classes?
     
  12. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    6,555
    Yes, I was able to confirm that. It looks like .NET wrapped StorageFile APIs in order to allow access to files under the hood if regular Windows API access failed. IL2CPP doesn't do that, so it fails with access denied exception. That is something I want to fix to match .NET behaviour.
     
  13. smenyhart

    smenyhart

    Joined:
    May 6, 2016
    Posts:
    35
    Ok I think I've finally found a workaround.

    You have to add "Windows::Storage::AccessCache::StorageApplicationPermissions::FutureAccessList->Add(file);" into the for loop in the OnFileActivated method of App.xaml.cpp.

    Then you can use the following code later to copy the file to the app specific temp folder. After which the copy can be accessed with regular IO classes.

    Code (CSharp):
    1. public class HoloLensExternalDataInput : MonoBehaviour
    2. {
    3.     // Use this for initialization
    4.     void Start ()
    5.     {
    6.         UnityEngine.WSA.Application.windowActivated += Application_windowActivated;
    7.     }
    8.  
    9.     private void Application_windowActivated(UnityEngine.WSA.WindowActivationState state)
    10.     {
    11.         if (state != UnityEngine.WSA.WindowActivationState.Deactivated)
    12.         {
    13.             var arguments = UnityEngine.WSA.Application.arguments;
    14.             Debug.Log("Application_windowActivated - arguments: " + arguments);
    15.  
    16.             const string FILE_ARG = "File=";
    17.             const string UIR_ARG = "Uri=";
    18.  
    19.             if (arguments.StartsWith(FILE_ARG))
    20.             {
    21.                 var filesStr = arguments.Substring(FILE_ARG.Length);
    22.                 var files = filesStr.Split(';');
    23.  
    24.                 if (files.Length > 0)
    25.                 {
    26. #if ENABLE_WINMD_SUPPORT
    27.                     LoadExternalFile(files[0]);
    28. #else
    29.                     DataLoadManager.LoadExternalFile(files[0]);
    30. #endif
    31.                 }
    32.             }
    33.             else if (arguments.StartsWith(UIR_ARG))
    34.             {
    35.                 var uri = arguments.Substring(UIR_ARG.Length);
    36.                 //TODO: handel URI
    37.             }
    38.         }
    39.     }
    40.  
    41. #if ENABLE_WINMD_SUPPORT
    42.     private async void LoadExternalFile(string filePath)
    43.     {
    44.         Windows.Storage.StorageFile storageFile = await Windows.Storage.StorageFile.GetFileFromPathAsync(filePath);
    45.  
    46.         Windows.Storage.StorageFile copiedFile = await storageFile.CopyAsync(Windows.Storage.ApplicationData.Current.TemporaryFolder, storageFile.Name, Windows.Storage.NameCollisionOption.ReplaceExisting);
    47.  
    48.         DataLoadManager.LoadExternalFile(copiedFile.Path);
    49.     }
    50. #endif
    51. }