Author Topic: Run synchronous Command in async method  (Read 475 times)

0 Members and 1 Guest are viewing this topic.

stw

  • Mosquito
  • Posts: 3
Run synchronous Command in async method
« on: March 24, 2022, 10:39:42 AM »
Hi,

I have some async methods (like web requests) that need to be called in-between a sequence of commands. I need the sub-commands to run synchronously, so cannot use CommandAsync(), because the sub-commands have user-interaction and I want to wait till all of that is finished (a sub-command e.g. could get a selection set).

However, whenever I try to use editor.Command() in an asynchronous part of an async function I get the following Exception:
Autodesk.AutoCAD.Runtime.Exception: eInvalidInput

Example:
Code: [Select]
    [CommandMethod("CMD_TEST")]
    public static async void CmdTest()
    {
        var activeDoc = Application.DocumentManager.MdiActiveDocument;
        Editor editor = activeDoc.Editor;

        await Task.Delay(100);   // this could be e.g. some async web request in practice
        try
        {
            editor.Command("BCOUNT");  // some dummy command
            editor.WriteMessage("finished CMD_TEST\n");
        }
        catch (Exception ex)
        {
            editor.WriteMessage("failed CMD_TEST\n");
        }
    }

What is the reason for that and how could it be solved?


For completeness, these are the two stacks of a working and non-working Command() call:
Code: [Select]
> test_autocad.dll!Tools.AutoCAD.CommandsTest.CmdTestWorking()
  accoremgd.dll!Autodesk.AutoCAD.Runtime.CommandClass.InvokeWorker(System.Reflection.MethodInfo mi, object commandObject, bool bLispFunction)
  accoremgd.dll!Autodesk.AutoCAD.Runtime.CommandClass.InvokeWorkerWithExceptionFilter(System.Reflection.MethodInfo mi, object commandObject, bool bLispFunction)
  accoremgd.dll!Autodesk.AutoCAD.Runtime.CommandClass.CommandThunk.Invoke()
  [Native to Managed Transition]
  accore.dll!CCmdThroat::arxCallNativeFunction()
  accore.dll!Command::doCommand()
  accore.dll!runTheCommand()
  accore.dll!CommandProcessor::mainCmdLoop()
  accore.dll!CCmdThroat::Run()
  accore.dll!AcApAppImp::internalRun()
  accore.dll!AcApAppImp::run(void)
  acad.exe!00007ff764026c41()
  mfc140u.dll!00007ff8109a3840()
  acad.exe!00007ff764039602()
  kernel32.dll!00007ff859ca7034()
  ntdll.dll!00007ff85b822651()



> test_autocad.dll!Tools.AutoCAD.CommandsTest.CmdTestFailing()
  [Resuming Async Method]
  mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)
  mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)
  mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()
  mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.OutputAsyncCausalityEvents.AnonymousMethod__0()
  mscorlib.dll!System.Runtime.CompilerServices.TaskAwaiter.OutputWaitEtwEvents.AnonymousMethod__0()
  mscorlib.dll!System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation.GetActionLogDelegate.AnonymousMethod__0()
  acpal.dll!00007fffe380d218()
  acpal.dll!00007fffe380d2e4()
  accore.dll!AcApAppImp::notifyMessagePreviewObservers(struct tagMSG &,bool)
  accore.dll!AcApApp::notifyMessagePreviewObservers(struct tagMSG &)
  acad.exe!CAcadApp::PreTranslateMessage()
  mfc140u.dll!AfxInternalPumpMessage()
  acad.exe!CAcadApp::PumpMessage(void)
  acad.exe!CAcadApp::runMessageLoop()
  accore.dll!AcApAppImp::runMessageLoop(void)
  accore.dll!win_event(void)
  accore.dll!CCmdThroat::select_device()
  accore.dll!CDig::digin()
  accore.dll!CAcCoreInput::GetRawInput()
  accore.dll!CAcCoreInput::egetdat()
  accore.dll!getCommandNameOrGripSelectionInput()
  accore.dll!CCmdThroat::Run()
  accore.dll!AcApAppImp::internalRun()
  accore.dll!AcApAppImp::run(void)
  acad.exe!CAcadApp::Run(void)
  mfc140u.dll!AfxWinMain(HINSTANCE__ * hInstance, HINSTANCE__ * hPrevInstance, wchar_t * lpCmdLine, int nCmdShow)
  acad.exe!__scrt_common_main_seh()
  kernel32.dll!BaseThreadInitThunk()
  ntdll.dll!RtlUserThreadStart()
« Last Edit: March 24, 2022, 01:08:41 PM by stw »

CADbloke

  • Bull Frog
  • Posts: 337
  • Crash Test Dummy
Re: Run synchronous Command in async method
« Reply #1 on: March 25, 2022, 03:45:46 AM »
The reason is AutoCAD runs on the UI thread, like most legacy software from the Stone Age.

The fix  probably involves adding the command to a list that is executed on the main thread after the async routine returns and the app is quiescent.

You could try Dispatcher.Invoke… I’m on a phone so I haven’t got the syntax handy. My guess is this method would be unpredictability unreliable. I wouldn’t trust it.

stw

  • Mosquito
  • Posts: 3
Re: Run synchronous Command in async method
« Reply #2 on: March 25, 2022, 12:41:14 PM »
Thank you for the answer. When awaiting a Task in C# it continues on the same thread by default (except if one's using ConfigureAwait(false) or similar things), so that shouldn't be an issue.

However, it is for sure some context thingy. I meanwhile found out that there's Application.DocumentManager.ExecuteInCommandContextAsync which allows to execute code in the context of a command. This theoretically solves my initial problem, but unfortunately brings up other ones instead. ExecuteInCommandContextAsync doesn't work, when it's already executed in a command context.

My main question is now whether there's any way I can (programatically) find out whether I'm currently in the correct context to execute a command or not (and use ExecuteInCommandContextAsync then). Something like Dispatcher.CheckAccess() or InvokeRequired known from WPF/WinForms when trying to access something on the UI thread.

A simple approach I could think of could be to simply try executing the command and if it fails with eInvalidInput try again with ExecuteInCommandContextAsync, but that doesn't seem like a good approach to me.

Is there any method to find out whether a piece of code is executing in a suitable context for invoking Command() or CommandAsync() ?

stw

  • Mosquito
  • Posts: 3
Re: Run synchronous Command in async method
« Reply #3 on: March 30, 2022, 10:25:34 AM »
Application.DocumentManager.IsApplicationContext seems to be suitable. When it returns true we aren't in a command context, and it also seems to always return false when we are already in a command context.