Архив рубрики: AutoCAD

О хостинге WCF-сервисов в accoreconsole.exe (AutoCAD 2016)

Как известно, WCF-сервисы могут в качестве хостинга использовать не только IIS и WAS, но так же и произвольные приложения (консольные или GUI). Как показывает практика, в качестве хоста можно использовать acad.exe. В идеале хотелось бы иметь возможность хостить службы в accoreconsole.exe, но не забываем, что это Autodesk, а это означает, что скучать не придётся...


Когда это может оказаться интересным?

Как известно, в параметрах запуска accoreconsole.exe можно указывать набор ключей, в т.ч. и ключ /s, при помощи которого разрешено передавать имя файла скрипта (SCR-файла). По завершению работы скрипта приложение так же автоматически завершит свою работу. Однако порой может возникнуть потребность интерактивного использования функционала, предоставляемого accoreconsole.exe (т.е. само по себе консольное окно при этом не требуется) не закрывая приложение столько времени, сколько потребуется (можно просто скрыть консольное окошко).

Хостинг службы в AutoCAD позволяет другим приложениям (т.н. клиентам) взаимодействовать с ним не прибегая к использованию AutoCAD COM API и при этом получая возможность задействовать возможности AutoCAD .NET API. Кроме того, в любой момент служба может быть перемещена на любой др. компьютер абсолютно прозрачно для клиентов.

В случае необходимости, клиент так же сможет отправлять службе произвольный набор команд, которые будут выполняться в AutoCAD на локальной или удалённо расположенной машине. Например с планшета, работающего под управлением OS Android пользователь сможет отправлять команды пакетной обработки чертежей (очистка, аудит, пересохранение и т.п.).

Это не означает, что на удалённой или локальной машинке обязательно должен быть постоянно запущен AutoCAD. Нет. Клиент может обратиться к службе, которая хостится в IIS или WAS с требованием что-то сделать в AutoCAD. Эта служба запускает AutoCAD (устанавливая видимость его окна в False) и в свою очередь является клиентом для другой службы, хостящейся в AutoCAD, передавая ей ваши запросы, а вам - её ответы. После того, как необходимый вам набор операций в AutoCAD будет выполнен служба, хостящаяся в IIS или WAS завершает работу AutoCAD и ждёт следующих обращений. В случае необходимости, параллельно может быть запущено несколько экземпляров AutoCAD, выполняющих каждый свою задачу, полученную от клиента. Для упрощения в данной теме я не использую промежуточную службу.

Сервис может, к примеру, проверять наличие обновлений (для AutoCAD и его расширений) на сервере компании и в случае их обнаружения сообщать об этом пользователю (или выполнять обновление - это на откуп администраторов CAD).

В идеале, конечно же, лучше всего на роль хоста службы подошёл бы accoreconsole.exe...


Служба

Предположим, что служба, предназначенная для хостинга в AutoCAD, реализует такой интерфейс:

namespace Bushman.CAD.Services {

    [ServiceContract(Namespace = "www.gpsm.ru")]
    interface IMyContract {
        [OperationContract]
        void Write(string msg); // Write message into AutoCAD command console.

        [OperationContract]
        string GetVersion(); // Get AutoCAD version
    }
}

Реализуем обозначенный интерфейс как-то так:

using cad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
using Autodesk.AutoCAD.ApplicationServices;

namespace Bushman.CAD.Services {
    class MyService : IMyContract {
        public string GetVersion() {
            return cad.Version.ToString();
        }

        public void Write(string msg) {
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if (null != doc) {
                doc.Editor.WriteMessage(msg);
            }
        }
    }

Никаких команд определять не будем, а инициализацию расширения выполним следующим образом:

using System;
using System.ServiceModel;
using cad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
usingAutodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;

[assembly: ExtensionApplication(typeof(Bushman.CAD.Services.ExtensionApplication))]

namespace Bushman.CAD.Services {
    public class ExtensionApplication : IExtensionApplication {

        static ServiceHost host = null;

        static ExtensionApplication() {
            try{
                host = new ServiceHost(typeof(MyService));
                host.Open();
                host.UnknownMessageReceived += Host_UnknownMessageReceived;
                AppDomain.CurrentDomain.ProcessExit += ProcessExit;
            }
            catch(System.Exception ex) {
                Document doc = cad.DocumentManager.MdiActiveDocument;
                if (null != doc) doc.Editor.WriteMessage(ex.Message);
            }
        }

        private static void Host_UnknownMessageReceived(object sender,
            UnknownMessageReceivedEventArgs e) {
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if(null != doc) doc.Editor.WriteMessage(e.Message.ToString());
        }

        private static void ProcessExit(object sender, EventArgs e) {
            if(null != host) host.Close();
        }

        public void Initialize() {
            string status = null == host ? "null" : host.State.ToString();
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if(null != doc) doc.Editor.WriteMessage("\nHost status: {0}.\n", status);
        }

        public void Terminate() {
            // Nothing is here.
        }
    }
}


Клиент

Клиента реализуем следующим образом:

using System;
using System.ServiceModel;
usingBushman.MyClient.ServiceReference1;

namespace Bushman.MyClient {
    classProgram {
        static void Main(string[] args) {
            Console.Title = "CAD client";
            try{
                using (MyContractClient client = new MyContractClient("http")) {
                    if (client.InnerChannel.State != CommunicationState.Faulted) {
                        client.Open();
                        string version = client.GetVersion();
                        Console.WriteLine("CAD version: {0}", version);
                        client.Write("Client said: Hello, AutoCAD.\n");
                    }
                }
            }
            catch(Exception ex) {
                Console.WriteLine(ex.Message);
            }

            Console.WriteLine("Press any key for exit...");
            Console.ReadKey();
        }
    }
}


Конфигурационные файлы acad.exe.config и accoreconsole.exe.config настраиваю на работу с обозначенной выше службой:


<configuration>

  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>

  <!--All assemblies in AutoCAD are fully trusted so there's no point generating publisher evidence-->
  <runtime>
    <generatePublisherEvidence enabled="false"/>
  </runtime>

  <system.serviceModel>
    <services>
      <service name="Bushman.CAD.Services.MyService" behaviorConfiguration="MEXGET">
        <host>
          <baseAddresses>
            <add baseAddress="http://win7x64ac2:8001"/>
          </baseAddresses>
        </host>
        <endpoint name="http"
                  binding="wsHttpBinding"
                  address="MyService"
                  bindingConfiguration="MyContract"
                  contract ="Bushman.CAD.Services.IMyContract">
        </endpoint>
      </service>
    </services>
    <bindings>
      <wsHttpBinding>
        <binding name="MyContract"/>
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MEXGET">
          <serviceMetadata httpGetEnabled="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>

    <diagnostics wmiProviderEnabled="true">
      <messageLogging
           logEntireMessage="true"
           logMalformedMessages="true"
           logMessagesAtServiceLevel="true"
           logMessagesAtTransportLevel="true"
           maxMessagesToLog="3000"
       />
    </diagnostics>

  </system.serviceModel>

  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel"
              switchValue="Information, ActivityTracing"
              propagateActivity="true" >
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="myUserTraceSource"
              switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add name="xml"
           type="System.Diagnostics.XmlWriterTraceListener"
                 initializeData="\\hyprostroy\dfs\Обмен\Бушман\logs\Service-Traces.svclog" />
    </sharedListeners>
  </system.diagnostics>
</configuration>


Конфигурационный файл клиента выглядит так:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
  </startup>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="http" />
      </wsHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://win7x64ac2:8001/MyService" binding="wsHttpBinding"
          bindingConfiguration="http" contract="ServiceReference1.IMyContract"
          name="http">
        <identity>
          <!-- WARNING: change the value according your user principal name. -->
          <userPrincipalName value="admin@hyprostr" />
        </identity>
      </endpoint>
    </client>

    <diagnostics wmiProviderEnabled="true">
      <messageLogging
           logEntireMessage="true"
           logMalformedMessages="true"
           logMessagesAtServiceLevel="true"
           logMessagesAtTransportLevel="true"
           maxMessagesToLog="3000"
       />
    </diagnostics>

  </system.serviceModel>

  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel"
              switchValue="Information, ActivityTracing"
              propagateActivity="true" >
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="myUserTraceSource"
              switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add name="xml"
           type="System.Diagnostics.XmlWriterTraceListener"
                 initializeData="\\hyprostroy\dfs\Обмен\Бушман\logs\Client-Traces.svclog" />
    </sharedListeners>
  </system.diagnostics>

</configuration>


Запускаем службу и клиента
Служба и клиент могут находится как на одном компьютере, так и на разных компьютерах в сети (или в Интернет). Если в качестве хоста использовать acad.exe, то всё нормально работает:



Но вот если в качестве хоста использовать accoreconsole.exe, то достучаться до сервиса не удастся даже с локального компьютера, на котором запущен хост сервиса. При этом сервис успешно запускается, но не доступен:




В логах клиента можно посмотреть описание проблемы:



По обозначенной теме мне ответил Августо Гонсалес:
Augusto Goncalves (API Evangelist at Autodesk):
As far as I remember trying, accoreconsole.exe doesn't accept new calls (from automation) after is open (but I haven't tried with WCF). Acad.exe is a little different... I don't believe accoreconsole will remain receiving calls after it was launched, it was designed to launch with a list of commands on the .scr file.
Если Августо прав, то это будет очередной, весьма досадный недостаток accoreconsole.exe...

Продолжение темы - здесь...

О хостинге WCF-сервисов в accoreconsole.exe (AutoCAD 2016)

Как известно, WCF-сервисы могут в качестве хостинга использовать не только IIS и WAS, но так же и произвольные приложения (консольные или GUI). Как показывает практика, в качестве хоста можно использовать acad.exe. В идеале хотелось бы иметь возможность хостить службы в accoreconsole.exe, но не забываем, что это Autodesk, а это означает, что скучать не придётся...


Когда это может оказаться интересным?

Как известно, в параметрах запуска accoreconsole.exe можно указывать набор ключей, в т.ч. и ключ /s, при помощи которого разрешено передавать имя файла скрипта (SCR-файла). По завершению работы скрипта приложение так же автоматически завершит свою работу. Однако порой может возникнуть потребность интерактивного использования функционала, предоставляемого accoreconsole.exe (т.е. само по себе консольное окно при этом не требуется) не закрывая приложение столько времени, сколько потребуется (можно просто скрыть консольное окошко).

Хостинг службы в AutoCAD позволяет другим приложениям (т.н. клиентам) взаимодействовать с ним не прибегая к использованию AutoCAD COM API и при этом получая возможность задействовать возможности AutoCAD .NET API. Кроме того, в любой момент служба может быть перемещена на любой др. компьютер абсолютно прозрачно для клиентов.

В случае необходимости, клиент так же сможет отправлять службе произвольный набор команд, которые будут выполняться в AutoCAD на локальной или удалённо расположенной машине. Например с планшета, работающего под управлением OS Android пользователь сможет отправлять команды пакетной обработки чертежей (очистка, аудит, пересохранение и т.п.).

Это не означает, что на удалённой или локальной машинке обязательно должен быть постоянно запущен AutoCAD. Нет. Клиент может обратиться к службе, которая хостится в IIS или WAS с требованием что-то сделать в AutoCAD. Эта служба запускает AutoCAD (устанавливая видимость его окна в False) и в свою очередь является клиентом для другой службы, хостящейся в AutoCAD, передавая ей ваши запросы, а вам - её ответы. После того, как необходимый вам набор операций в AutoCAD будет выполнен служба, хостящаяся в IIS или WAS завершает работу AutoCAD и ждёт следующих обращений. В случае необходимости, параллельно может быть запущено несколько экземпляров AutoCAD, выполняющих каждый свою задачу, полученную от клиента. Для упрощения в данной теме я не использую промежуточную службу.

Сервис может, к примеру, проверять наличие обновлений (для AutoCAD и его расширений) на сервере компании и в случае их обнаружения сообщать об этом пользователю (или выполнять обновление - это на откуп администраторов CAD).

В идеале, конечно же, лучше всего на роль хоста службы подошёл бы accoreconsole.exe...


Служба

Предположим, что служба, предназначенная для хостинга в AutoCAD, реализует такой интерфейс:

namespace Bushman.CAD.Services {

    [ServiceContract(Namespace = "www.gpsm.ru")]
    interface IMyContract {
        [OperationContract]
        void Write(string msg); // Write message into AutoCAD command console.

        [OperationContract]
        string GetVersion(); // Get AutoCAD version
    }
}

Реализуем обозначенный интерфейс как-то так:

using cad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
using Autodesk.AutoCAD.ApplicationServices;

namespace Bushman.CAD.Services {
    class MyService : IMyContract {
        public string GetVersion() {
            return cad.Version.ToString();
        }

        public void Write(string msg) {
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if (null != doc) {
                doc.Editor.WriteMessage(msg);
            }
        }
    }

Никаких команд определять не будем, а инициализацию расширения выполним следующим образом:

using System;
using System.ServiceModel;
using cad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
usingAutodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;

[assembly: ExtensionApplication(typeof(Bushman.CAD.Services.ExtensionApplication))]

namespace Bushman.CAD.Services {
    public class ExtensionApplication : IExtensionApplication {

        static ServiceHost host = null;

        static ExtensionApplication() {
            try{
                host = new ServiceHost(typeof(MyService));
                host.Open();
                host.UnknownMessageReceived += Host_UnknownMessageReceived;
                AppDomain.CurrentDomain.ProcessExit += ProcessExit;
            }
            catch(System.Exception ex) {
                Document doc = cad.DocumentManager.MdiActiveDocument;
                if (null != doc) doc.Editor.WriteMessage(ex.Message);
            }
        }

        private static void Host_UnknownMessageReceived(object sender,
            UnknownMessageReceivedEventArgs e) {
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if(null != doc) doc.Editor.WriteMessage(e.Message.ToString());
        }

        private static void ProcessExit(object sender, EventArgs e) {
            if(null != host) host.Close();
        }

        public void Initialize() {
            string status = null == host ? "null" : host.State.ToString();
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if(null != doc) doc.Editor.WriteMessage("\nHost status: {0}.\n", status);
        }

        public void Terminate() {
            // Nothing is here.
        }
    }
}


Клиент

Клиента реализуем следующим образом:

using System;
using System.ServiceModel;
usingBushman.MyClient.ServiceReference1;

namespace Bushman.MyClient {
    classProgram {
        static void Main(string[] args) {
            Console.Title = "CAD client";
            try{
                using (MyContractClient client = new MyContractClient("http")) {
                    if (client.InnerChannel.State != CommunicationState.Faulted) {
                        client.Open();
                        string version = client.GetVersion();
                        Console.WriteLine("CAD version: {0}", version);
                        client.Write("Client said: Hello, AutoCAD.\n");
                    }
                }
            }
            catch(Exception ex) {
                Console.WriteLine(ex.Message);
            }

            Console.WriteLine("Press any key for exit...");
            Console.ReadKey();
        }
    }
}


Конфигурационные файлы acad.exe.config и accoreconsole.exe.config настраиваю на работу с обозначенной выше службой:


<configuration>

  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>

  <!--All assemblies in AutoCAD are fully trusted so there's no point generating publisher evidence-->
  <runtime>
    <generatePublisherEvidence enabled="false"/>
  </runtime>

  <system.serviceModel>
    <services>
      <service name="Bushman.CAD.Services.MyService" behaviorConfiguration="MEXGET">
        <host>
          <baseAddresses>
            <add baseAddress="http://win7x64ac2:8001"/>
          </baseAddresses>
        </host>
        <endpoint name="http"
                  binding="wsHttpBinding"
                  address="MyService"
                  bindingConfiguration="MyContract"
                  contract ="Bushman.CAD.Services.IMyContract">
        </endpoint>
      </service>
    </services>
    <bindings>
      <wsHttpBinding>
        <binding name="MyContract"/>
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MEXGET">
          <serviceMetadata httpGetEnabled="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>

    <diagnostics wmiProviderEnabled="true">
      <messageLogging
           logEntireMessage="true"
           logMalformedMessages="true"
           logMessagesAtServiceLevel="true"
           logMessagesAtTransportLevel="true"
           maxMessagesToLog="3000"
       />
    </diagnostics>

  </system.serviceModel>

  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel"
              switchValue="Information, ActivityTracing"
              propagateActivity="true" >
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="myUserTraceSource"
              switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add name="xml"
           type="System.Diagnostics.XmlWriterTraceListener"
                 initializeData="\\hyprostroy\dfs\Обмен\Бушман\logs\Service-Traces.svclog" />
    </sharedListeners>
  </system.diagnostics>
</configuration>


Конфигурационный файл клиента выглядит так:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
  </startup>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="http" />
      </wsHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://win7x64ac2:8001/MyService" binding="wsHttpBinding"
          bindingConfiguration="http" contract="ServiceReference1.IMyContract"
          name="http">
        <identity>
          <!-- WARNING: change the value according your user principal name. -->
          <userPrincipalName value="admin@hyprostr" />
        </identity>
      </endpoint>
    </client>

    <diagnostics wmiProviderEnabled="true">
      <messageLogging
           logEntireMessage="true"
           logMalformedMessages="true"
           logMessagesAtServiceLevel="true"
           logMessagesAtTransportLevel="true"
           maxMessagesToLog="3000"
       />
    </diagnostics>

  </system.serviceModel>

  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel"
              switchValue="Information, ActivityTracing"
              propagateActivity="true" >
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="myUserTraceSource"
              switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add name="xml"
           type="System.Diagnostics.XmlWriterTraceListener"
                 initializeData="\\hyprostroy\dfs\Обмен\Бушман\logs\Client-Traces.svclog" />
    </sharedListeners>
  </system.diagnostics>

</configuration>


Запускаем службу и клиента
Служба и клиент могут находится как на одном компьютере, так и на разных компьютерах в сети (или в Интернет). Если в качестве хоста использовать acad.exe, то всё нормально работает:



Но вот если в качестве хоста использовать accoreconsole.exe, то достучаться до сервиса не удастся даже с локального компьютера, на котором запущен хост сервиса. При этом сервис успешно запускается, но не доступен:




В логах клиента можно посмотреть описание проблемы:



По обозначенной теме мне ответил Августо Гонсалес:
Augusto Goncalves (API Evangelist at Autodesk):
As far as I remember trying, accoreconsole.exe doesn't accept new calls (from automation) after is open (but I haven't tried with WCF). Acad.exe is a little different... I don't believe accoreconsole will remain receiving calls after it was launched, it was designed to launch with a list of commands on the .scr file.
Если Августо прав, то это будет очередной, весьма досадный недостаток accoreconsole.exe...

Продолжение темы - здесь...

Блокировка кнопки и контекстного меню закрытия консольного окна

Как известно, accoreconsole.exe всегда был и до сих пор остаётся достаточно кривым... Один из неприятных аспектов его поведения, присутствующий по сей день, заключается в том, что если завершать работу приложения кликом мышки по кнопке закрытия консольного окна в верхнем правом углу, либо выбирая соответствующий пункт из контекстного меню консольного окна, то приложение завершает свою работу через задницу - не выполняя код методов Terminate(), а так же код зарегистрированных событий, таких например, как AppDomain.CurrentDomain.ProcessExit.

Однако обозначенная проблема гораздо глубже и не ограничивается рамками кода ваших расширений: при таком способе закрытия AutoCAD так же не выполняет и свой собственный код, который он обычно выполняет при завершении работы приложения (код корректного освобождения ресурсов, сохранения настроек и т.п.). Например, не происходит восстановление настроек в реестре, которые временно были изменены accoreconsole.exe под свои нужды. Это сразу бросается в глаза на напримере переменной FILEDIA, на время работы консольного приложения устанавливается в 0:  при очередном запуске acad.exe для неё приходится вручную восстанавливать значение 1 (в противном случае вместо диалоговых окон AutoCAD будет использовать свою консоль).

Если завершать работу accoreconsole.exe путём вызова команд quit и exit, то завершение работы приложения происходит так, как это должно было происходить (т.е. выполняется весь необходимый код). Однако никто не застрахован от клика мышкой по обозначенной выше кнопке, а пользователи с вероятностью 100% будут клацать как раз именно по ней, когда потребуется завершить работу приложения, потому как такой способ завершения работы - самый простой.

В качестве "лекарства" против обозначенной выше проблемы я блокирую кнопку закрытия консольного окна и соответствующий её пункт контекстного меню:


  1. const uint MF_BYCOMMAND = 0x00000000;
  2. const uint MF_GRAYED = 0x00000001;
  3. const uint SC_CLOSE = 0xF060;
  4. const uint MF_DISABLED = 0x00000002;
  5. [DllImport("kernel32.dll")]
  6. static extern IntPtr GetConsoleWindow();
  7. [DllImport("user32.dll")]
  8. static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
  9. [DllImport("User32.dll", SetLastError = true)]
  10. static extern uint EnableMenuItem(IntPtr hMenu, uint itemId, uint uEnable);
  11. [DllImport("user32.dll")]
  12. static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
  13. ...
  14. // Disable the Close ("X") button and "Close" context menu item of the Console window
  15. IntPtr hwnd = GetConsoleWindow();
  16. IntPtr hmenu = GetSystemMenu(hwnd, false);
  17. uint hWindow = EnableMenuItem(hmenu, SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
  18. // Also it is possible to delete "Close" context menu item
  19. // instead of disabling it.
  20. // DeleteMenu(hmenu, SC_CLOSE, MF_BYCOMMAND);

Однако по факту я вижу, что кнопка закрытия окна заблокирована, а вот контекстное меню - нет... Конечно, можно попросту вовсе удалить этот пункт из контекстного меню и не заморачиваться на эту тему (в обозначенном выше примере кода это успешно делает последняя закомментированная строчка).

Однако мне всё же интересно: почему не блокируется пункт меню?

Оказалось, что обозначенная проблема свойственна не только accoreconsole.exe, но и любому консольному приложению в Windows 7 x64, а так же в Windows Server 2003. А вот в Windows 10 x64 всё работает корректно...

Т.о. то, что контекстное меню не блокируется в некоторых версиях Windows - очень похоже на баг WinAPI.

 В этой же теме сразу размещаю код примера того, как можно скрывать или отображать консольное окно (например всё тот же accoreconsole.exe):
  1. [DllImport("kernel32.dll")]
  2. static extern IntPtr GetConsoleWindow();
  3. [DllImport("user32.dll")]
  4. static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
  5. const int SW_HIDE = 0;
  6. const int SW_SHOW = 5;
  7. IntPtr hwnd = GetConsoleWindow();
  8. // Hide window
  9. ShowWindow(hwnd, SW_HIDE);
  10. // Show window
  11. ShowWindow(hwnd, SW_SHOW);

Длительная, нередко печальная практика показывает, что лозунг Autodesk касательно данного продукта, к сожалению, выглядит как-то так:
`accoreconsole.exe` - мы заставим Вас работать через задницу!

Блокировка кнопки и контекстного меню закрытия консольного окна

Как известно, accoreconsole.exe всегда был и до сих пор остаётся достаточно кривым... Один из неприятных аспектов его поведения, присутствующий по сей день, заключается в том, что если завершать работу приложения кликом мышки по кнопке закрытия консольного окна в верхнем правом углу, либо выбирая соответствующий пункт из контекстного меню консольного окна, то приложение завершает свою работу через задницу - не выполняя код методов Terminate(), а так же код зарегистрированных событий, таких например, как AppDomain.CurrentDomain.ProcessExit.

Однако обозначенная проблема гораздо глубже и не ограничивается рамками кода ваших расширений: при таком способе закрытия AutoCAD так же не выполняет и свой собственный код, который он обычно выполняет при завершении работы приложения (код корректного освобождения ресурсов, сохранения настроек и т.п.). Например, не происходит восстановление настроек в реестре, которые временно были изменены accoreconsole.exe под свои нужды. Это сразу бросается в глаза на напримере переменной FILEDIA, на время работы консольного приложения устанавливается в 0:  при очередном запуске acad.exe для неё приходится вручную восстанавливать значение 1 (в противном случае вместо диалоговых окон AutoCAD будет использовать свою консоль).

Если завершать работу accoreconsole.exe путём вызова команд quit и exit, то завершение работы приложения происходит так, как это должно было происходить (т.е. выполняется весь необходимый код). Однако никто не застрахован от клика мышкой по обозначенной выше кнопке, а пользователи с вероятностью 100% будут клацать как раз именно по ней, когда потребуется завершить работу приложения, потому как такой способ завершения работы - самый простой.

В качестве "лекарства" против обозначенной выше проблемы я блокирую кнопку закрытия консольного окна и соответствующий её пункт контекстного меню:


  1. const uint MF_BYCOMMAND = 0x00000000;
  2. const uint MF_GRAYED = 0x00000001;
  3. const uint SC_CLOSE = 0xF060;
  4. const uint MF_DISABLED = 0x00000002;
  5. [DllImport("kernel32.dll")]
  6. static extern IntPtr GetConsoleWindow();
  7. [DllImport("user32.dll")]
  8. static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
  9. [DllImport("User32.dll", SetLastError = true)]
  10. static extern uint EnableMenuItem(IntPtr hMenu, uint itemId, uint uEnable);
  11. [DllImport("user32.dll")]
  12. static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
  13. ...
  14. // Disable the Close ("X") button and "Close" context menu item of the Console window
  15. IntPtr hwnd = GetConsoleWindow();
  16. IntPtr hmenu = GetSystemMenu(hwnd, false);
  17. uint hWindow = EnableMenuItem(hmenu, SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
  18. // Also it is possible to delete "Close" context menu item
  19. // instead of disabling it.
  20. // DeleteMenu(hmenu, SC_CLOSE, MF_BYCOMMAND);

Однако по факту я вижу, что кнопка закрытия окна заблокирована, а вот контекстное меню - нет... Конечно, можно попросту вовсе удалить этот пункт из контекстного меню и не заморачиваться на эту тему (в обозначенном выше примере кода это успешно делает последняя закомментированная строчка).

Однако мне всё же интересно: почему не блокируется пункт меню?

Оказалось, что обозначенная проблема свойственна не только accoreconsole.exe, но и любому консольному приложению в Windows 7 x64, а так же в Windows Server 2003. А вот в Windows 10 x64 всё работает корректно...

Т.о. то, что контекстное меню не блокируется в некоторых версиях Windows - очень похоже на баг WinAPI.

 В этой же теме сразу размещаю код примера того, как можно скрывать или отображать консольное окно (например всё тот же accoreconsole.exe):
  1. [DllImport("kernel32.dll")]
  2. static extern IntPtr GetConsoleWindow();
  3. [DllImport("user32.dll")]
  4. static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
  5. const int SW_HIDE = 0;
  6. const int SW_SHOW = 5;
  7. IntPtr hwnd = GetConsoleWindow();
  8. // Hide window
  9. ShowWindow(hwnd, SW_HIDE);
  10. // Show window
  11. ShowWindow(hwnd, SW_SHOW);

Длительная, нередко печальная практика показывает, что лозунг Autodesk касательно данного продукта, к сожалению, выглядит как-то так:
`accoreconsole.exe` - мы заставим Вас работать через задницу!

Хостинг PowerShell в AutoCAD

На Bitbucket опубликовал пример хостинга PowerShell в AutoCAD. Такой хостинг позволяет программировать в AutoCAD на PowerShell путём использования AutoCAD .NET API. Демонстрационное видео здесь. Откомпилированная под AutoCAD 2016 версия проекта тут.



В проекте продемострировано использования двух подходов в программировании под AutoCAD:

1. Программирование на PowerShell.
2. Динамическая компиляция исходников C# с автоматической загрузкой и возможностью последующего выполнения скомпилированного кода. Аналогичный пример на VB.NET показывать не буду, т.к. там всё происходит аналогичным образом.

Дополнительная информация для размышления: хостинг PowerShell в AutoCAD может оказаться полезным в т.ч. и для программистов, пишущих на AutoLISP\Visual LISP, т.к. помимо доступа к различным технологиям и платформам от Майкрософт, дополнительно предосталяет им возможность в Lisp-коде пользоваться .NET-библиотеками, в т.ч. выполнять динамическую компиляцию произвольного .NET-кода с последующим его исполнением.

Рассказывать о том, что такое PowerShell и зачем он нужен не буду - желающие смогут без труда сами найти информацию на эту тему в Интернете.

Хостинг PowerShell в AutoCAD

На Bitbucket опубликовал пример хостинга PowerShell в AutoCAD. Такой хостинг позволяет программировать в AutoCAD на PowerShell путём использования AutoCAD .NET API. Демонстрационное видео здесь. Откомпилированная под AutoCAD 2016 версия проекта тут.



В проекте продемострировано использования двух подходов в программировании под AutoCAD:

1. Программирование на PowerShell.
2. Динамическая компиляция исходников C# с автоматической загрузкой и возможностью последующего выполнения скомпилированного кода. Аналогичный пример на VB.NET показывать не буду, т.к. там всё происходит аналогичным образом.

Дополнительная информация для размышления: хостинг PowerShell в AutoCAD может оказаться полезным в т.ч. и для программистов, пишущих на AutoLISP\Visual LISP, т.к. помимо доступа к различным технологиям и платформам от Майкрософт, дополнительно предосталяет им возможность в Lisp-коде пользоваться .NET-библиотеками, в т.ч. выполнять динамическую компиляцию произвольного .NET-кода с последующим его исполнением.

Рассказывать о том, что такое PowerShell и зачем он нужен не буду - желающие смогут без труда сами найти информацию на эту тему в Интернете.

Быстрый способ проверить наличие запущенного процесса acad.exe или accoreconsole.exe

То, что предоставлено в данной заметке не является документированным способом и получено на основе анализирования состава мьютексов, создаваемых и используемых AutoCAD в процессе своей работы. Способ применим как к acad.exe, так и к accoreconsole.exe. Проверялся с AutoCAD 2009-2016 x64, а так же с AutoCAD 2008 x86, запущенном в Windows x64. Решение продемонстрировано в двух вариантах: C++ и C#.

В .NET получить процессы по имени можно при помощи статического метода Process.GetProcessesByName("acad"). Однако, если переименовать любой EXE файл в acad.exe, то  будучи запущенным, он тоже попадёт в выборку, возврашаемую этим методом, хотя на самом деле это не процесс AutoCAD.

Конечно же, приведённый в этой заметке код не является "серебрянной пулей", т.к. всегда можно программно создать Mutex с указанным в нашем коде именем... Однако, обозначенный мною вариант проверки не удастся обмануть простым переименованием любого EXE файла в acad.exe или accoreconsole.exe, поэтому он, как мне кажется, имеет право на жизнь...

Как это сделать на C++:


/*  © Andrey Bushman, 2015
    http://bushman-andrey.blogspot.ru/2015/11/acadexe-accoreconsoleexe.html


    This is the quick way of checking is any AutoCAD launched (acad.exe
    or accoreconsole.exe) or not. I've checked my code for the usual
    AutoCAD 2009-2016 x64. But I haven't their x86 versions, I haven't
    older AutoCAD versions, and I haven't their vertical products,
    therefore I can't check my code for these versions.

 
    Additionally, Alexander Rivilis checked this code for AutoCAD 2008 x86
    which was launched in Windows x64.


    NOTE
    Visual C++ Redistributable for Visual Studio (your version) must be
    installed on the target computers.
*/

#include<Windows.h>
#include<iostream>
#include<exception>
#include<string>
#include<tchar.h>
#include<strsafe.h>

using namespace std;

#define UNEXPECTED_EXCEPTION 1
#define UNKNOWN_ERROR 2

BOOL IsLaunchedAnyAutoCAD();

int wmain(int argc, wchar_t *argv[])
try {
    // setlocale(LC_ALL, "Russian");

    SetConsoleTitle(TEXT("Is any AutoCAD launched?"));

    BOOL result = IsLaunchedAnyAutoCAD();

    if (result) {
        wcout << L"Any AutoCAD is launched." << endl;
    }
    else {
        wcout << L"Any AutoCAD is not launched." << endl;
    }   

    wcout << L"Press 'x' for exit..." << endl;
    wchar_t c;
    wcin >> c;
}
catch (exception ex) {
    wcerr << ex.what() << endl;
    return UNEXPECTED_EXCEPTION;
}
catch(...){
    wcerr << L"Unknown error." << endl;
    return UNKNOWN_ERROR;
}

BOOL IsLaunchedAnyAutoCAD() {
    BOOL result = FALSE;
    LPCTSTR anyAcadMutexName = TEXT(
        // This works for AutoCAD 2008 and newer (I haven't older
        // AutoCAD versions, therefore I can't check it for them).
        "Global\\8C84DAD6-9865-400e-A6E3-686A61C16968"

        // This is for AutoCAD 2009 and newer
        // "Local\\AcadProfileStorage_54519085-6DDA-4070-BB93-3A095D7E1140"
        );
    HANDLE hAnyAcadMutex = OpenMutex(READ_CONTROL, FALSE, anyAcadMutexName);
    if (NULL != hAnyAcadMutex) {
        result = TRUE;
        CloseHandle(hAnyAcadMutex);
    }
    return (result);
}



Как это сделать на C#:


/*  © Andrey Bushman, 2015
    http://bushman-andrey.blogspot.ru/2015/11/acadexe-accoreconsoleexe.html

    This is the quick way of checking is any AutoCAD launched (acad.exe
    or accoreconsole.exe) or not. I've checked my code for the usual
    AutoCAD 2009-2016 x64. But I haven't their x86 versions, I haven't
    older AutoCAD versions, and I haven't their vertical products,
    therefore I can't check my code for these versions.


    Additionally, Alexander Rivilis checked this code for AutoCAD 2008 x86
    which was launched in Windows x64.
*/
using System;
using System.Threading;

namespace Bushman.Sandbox.AutoCAD
{
    class Program
    {
        static Boolean IsAnyAutoCadLaunched()
        {
            try
            {
                Mutex m = Mutex.OpenExisting(
                    // This works for AutoCAD 2008 and newer (I haven't older
                    // AutoCAD versions, therefore I can't check it for them).
                    "Global\\8C84DAD6-9865-400e-A6E3-686A61C16968"

                    // This is for AutoCAD 2009 and newer
                    // "Local\\AcadProfileStorage_54519085-6DDA-4070-BB93-3A095D7E1140"
                    );
                m.Close();
                return true;
            }
            catch
            {
                return false;
            }
        }


        static void Main(string[] args)
        {
            String msg = IsAnyAutoCadLaunched() ? "Any AutoCAD is launched." :
                "Any AutoCAD is not launched.";

            Console.WriteLine(msg);

            Console.WriteLine("Press any key for exit...");
            Console.ReadKey();
        }
    }
}




Открытие и закрытие консольного окна для GUI-приложения

Работая с GUI приложением иногда бывает удобно в режиме реального времени посмотреть, что оно отправляет себе на консоль (т.е. в потоки stdout и stderr), а порой может возникнуть и желание что-то отправить в поток stdin с клавиатуры. Можно, конечно же, выполнять перенаправление в файлы, но этот вариант не всегда удобен. В данной заметке, на примере AutoCAD, показано, как для GUI-приложения открыть консольное окно, выполнить перенаправление потоков и, после того как консольное окно не будет нужно, закрыть его.


Пример C++ кода, выполняющего открытие консольного окна и перенаправляющие потоки ввода-вывода:

// I open console window for AutoCAD GUI application
BOOL result = AllocConsole();

if (0 == result){
    DWORD errCode = GetLastError();
    LPTSTR msg = NULL;
    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
        | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);

    acutPrintf(_T("\nAllocConsole Error: %s"), msg);
    HeapFree(GetProcessHeap(), 0, msg);
}
else{
    acutPrintf(_T("\nAllocConsole: OK."));
    SetConsoleTitle(L"AutoCAD console window");

    // Disable the close button of the Console window
    HWND hwnd = GetConsoleWindow();
    HMENU hmenu = GetSystemMenu(hwnd, FALSE);
    EnableMenuItem(hmenu, SC_CLOSE, MF_GRAYED);

    // redirecting of the streams
    freopen("CONOUT$", "w", stdout);
    freopen("CONOUT$", "w", stderr);
    freopen("CONIN$", "r", stdin);

    // You will see this messages in the console window
    wcout << L"wcout ping..." << endl;
    wcerr << L"wcerr ping..." << endl;
    cout << "cout ping..." << endl;
    cerr << "cerr ping..." << endl;

    // check wcin accessibility
    wcout << L"Press any char: ";
    wchar_t c;
    wcin >> c;
    wcout << L"You pressed the '" << c << "' char." << endl;
}


Результат выглядит следующим образом:

После того, как консольное окно станет ненужным, его можно закрыть. Однако его нельзя закрывать обычным кликом мышки по кнопке "X" в верхнем правом углу, иначе это приведёт к аварийному завершению работы GUI-приложения (в нашем примере - AutoCAD). Для того, чтобы пользователь случайно не нажал этой кнопки, в обозначенном выше коде мы делаем её недоступной.

Закрытие консольного окна выполняем так же программно:


BOOL result = FreeConsole();

if (0 == result){
    DWORD errCode = GetLastError();
    LPTSTR msg = NULL;
    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
        | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);

    acutPrintf(_T("\nFreeConsole Error: %s"), msg);
    HeapFree(GetProcessHeap(), 0, msg);
}
else{
    // redirecting of the streams
    freopen("CONOUT$", "w", stdout);
    freopen("CONOUT$", "w", stderr);
    freopen("CONIN$", "r", stdin);
}





Опубликован проект AcadKeyParser

Выложил на BitBucket проект AcadKeyParser. Информацию о назначении библиотеки, примеры её использования и откомпилированные версии под .NET 3.5, 4.0, 4.5 и 4.6 разместил там же. В составе решения присутствуют модульные тесты (NUnit) и консольная утилита, позволяющая интерактивно проверять "валидность" ключей AutoCAD. Результаты модульных тестов автоматом оформляются в виде HTML отчёта при помощи утилиты ReportUnit (более детальную информацию см. в файле run_me.bat).

NUnit: Тестирование DLL, использующей внешние данные

В составе одной из моих DLL присутствует функционал, позволяющий проверять на предмет корректности выражения вида ACAD-E001:409 (а так же создавать их) и, если выражение корректно, то полностью расшифровывать эту информацию. Для работы обозначенной DLL необходим специальный XML файл, содержащий информацию о существующих версиях AutoCAD и их вертикальных продуктах. Этот файл размещается в том же каталоге, в котором находится DLL. Для тестирования обозначенного функционала я написал пару тестов и запустил их в GUI NUnit...


Код моей DLL находит нужный XML следующим образом:

String asmLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly(
  ).Location);

// xml file with AutoCAD products data.
String xmlFile = Path.Combine(asmLocation, "cad.data.xml");
if (!File.Exists(xmlFile)) {
  throw new FileNotFoundException(xmlFile);
}

Результаты компиляции размещены в каталоге "C:\Users\developer\Documents\Visual Studio 2013\Projects\cad-solution\cad.UnitTests\bin\Debug". Запустив тесты на исполнение я увидел, что все они с треском провалились:


В правом верхнем текстбоксе читаю сгенерированное моим кодом сообщение об ошибке:

Bushman.CAD.UnitTests.Tests.LocalizedProduct_TryParse_Check_BoolValue(null,False):
System.TypeInitializationException : Инициализатор типа "Bushman.CAD.LocalizedProduct" выдал исключение.
  ----> System.IO.FileNotFoundException : C:\Users\developer\AppData\Local\Temp\nunit20\ShadowCopyCache\5128_635702471073123992\Tests_27930185\assembly\dl3\7e84bc6f\b85de2a2_d6a9d001\cad.data.xml

Я подсветил красным цветом ключевую информацию. Это исключение происходит потому, что NUnit копирует тестируемую DLL в произвольно сгенерированный каталог и уже оттуда загружает её и запускает присутствующие в ней тесты. Но скопировав DLL, NUnit не сделал того же самого и для моего XML файла, в виду чего и происходит обозначенное исключение. Обозначенное поведение в NUnit называется Shadow Copy (Теневое Копирование).

Shadow Copy можно включать или отключать в настройках NUnit: меню Tools -> Settings... -> Test Loader -> Advanced -> Shadow Copy -> Enable Shadow Copy:


Обратите внимание на информацию, которую я выделил красным цветом.

Если обозначенную галочку снять, то после перезапуска NUnit теневое копирование не происходит и DLL загружается из того каталога, где она изначально находилась. Соответственно успешно находится и XML файл: