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

Динамический вызов функций неуправляемых DLL

В .NET-атрибутах можно указывать только константные выражения. Т.о. атрибуту DllImport нужно указывать имя библиотеки статически, дабы это имя было известно на этапе компиляции. Порой это порождает уродливые конструкции, разрастающиеся как снежный ком, по мере появления новых версий AutoCAD. Наглядный тому пример можно увидеть здесь в коде Александра Ривилиса: по мере появления AutoCAD 2018, 2019 и т.д. - этот код придётся каждый раз дописывать.


Как правило, имя нужной DLL достаточно просто выяснить во время выполнения кода. Например, в обозначенном выше коде Александра Ривилиса нужная функция хранится в файлах acdb17.dll, acdb18.dll, ac1st19.dll, ac1st20.dll, ac1st21.dll. Т.е. мы видим, что каждая библиотека в качестве суффикса использует значение Major версии приложения. Кроме того, мы видим, что начиная с версии 19 (AutoCAD 2013) функция была перенесена в др. библиотеку (т.е. изменился префикс в её имени). Зная обозначенные выше правила, можно динамически вычислить имя DLL файла, в котором находится интересующая нас функция.

Далее показываю небольшой пример того, как можно динамически вызывать функцию acedSetEnv в разных версиях AutoCAD, не создавая для неё статических обёрток. На всякий случай напоминаю, что до версии AutoCAD 2013 эта функция находилась в файле acad.exe, а начиная с AutoCAD 2013 была перенесена в accore.dll.

Код проверялся в AutoCAD 2009 и 2016.

-------------------------------------------------------------
/* Utils.cs */
using System;
using System.Text;
using System.Runtime.InteropServices;
using cad = Autodesk.AutoCAD.ApplicationServices.Application;

namespace Bushman.AutoCAD.Sandbox {

    [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet
        = CharSet.Auto)]
    public delegate int acedSetEnvDelegate(string envName,
        StringBuilder NewValue);

    public static class Utils {

        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string
            dllToLoad);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr
            hModule, string procedureName);


        [DllImport("kernel32.dll")]
        public static extern bool FreeLibrary(IntPtr hModule);

        const int AutoCAD_2013_Major = 19;

        public static string GetDllName() {

            if (cad.Version.Major < AutoCAD_2013_Major)
                return "acad.exe";
            else
                return "accore.dll";
        }

        public static acedSetEnvDelegate acedSetEnv;

        static Utils() {
            acedSetEnv = GetAcedSetEnv();
        }

        static acedSetEnvDelegate GetAcedSetEnv() {

            string dllName = Utils.GetDllName();
            IntPtr pDll = Utils.LoadLibrary(dllName);

            if (pDll == IntPtr.Zero) {
                return null;
            }

            string funcName = "acedSetEnv";

            IntPtr pAddressOfFunctionToCall = Utils
                .GetProcAddress(pDll, funcName);

            if (pAddressOfFunctionToCall == IntPtr.Zero) {

                return null;
            }

            acedSetEnvDelegate acedSetEnv = (
                acedSetEnvDelegate) Marshal
                .GetDelegateForFunctionPointer(
                pAddressOfFunctionToCall,
                typeof(acedSetEnvDelegate));

            bool result = Utils.FreeLibrary(pDll);

            return acedSetEnv;
        }
    }
}

 ------------------------------------------------------------

Далее создадим тестовую команду, которая изменит значение переменной "ACAD":

------------------------------------------------------------
/* Commands.cs */
using System;
using System.Runtime.InteropServices;
using System.Text;
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;

namespace Bushman.AutoCAD.Sandbox {

    public sealed class Commands {

        [CommandMethod("Test", CommandFlags.Modal)]
        public void Test() {

            Document doc = cad.DocumentManager
                .MdiActiveDocument;

            if (doc == null)
                return;

            Editor ed = doc.Editor;

            acedSetEnvDelegate acedSetEnv = Utils.acedSetEnv;

            if (acedSetEnv == null) {
                ed.WriteMessage("acedSetEnv function was " +
                    "not found.");
                return;
            }

            /* For example, we'll edit the "Support File Search
             * Path" value: */
            string varName = "ACAD";
            StringBuilder sb = new StringBuilder(
                @"C:\abc\def");

            int theResult = acedSetEnv(varName, sb); // 5100
        }
    }
}

---------------------------------------------------------------

Динамическое связывание и изменение системой переменной ACAD успешно происходит в обоих тестируемых версиях AutoCAD: 2009 и 2016.



NuGet пакеты для AutoCAD .NET API

Пару лет назад, компания Autodesk начала (наконец-то!) опубликовывать NuGet-пакеты для AutoCAD .NET API. Об этом так же было радостно сообщено в блоге ADN. Однако, как это обычно и бывает у Autodesk, тестированием пакетов перед их публикацией в Autodesk не заморачиваются (как собственно и тестированием самого AutoCAD, ибо длительная, печальная практика показывает, что группа тестирования даже самого AutoCAD в компании Autodesk в принципе нарисована только "для галочки" - примеров, доказывающих это - множество, в т.ч. и в моём блоге).

 Пробежавшись по комментариям, оставленным пользователями в обозначенной выше записи блога ADN можно сразу заметить, что пакеты работают криво. Т.е. их тестированием себя никто не утруждал. С тех пор Autodesk выпустил ещё две версии AutoCAD. Казалось бы, что за два года можно было вполне довести до ума пакеты, на создание которых (с нуля) уходит не более получаса (причём сразу под все версии AutoCAD от 2009 и выше)... Но печальная практика в который раз демонстрирует обратное...

Я попробовал установить самую свежую на сегодняшний день версию пакета AutoCAD.NET (version 21.0.1). В душе теплилась надежда, что за два года проблему с CopyLocal в Autodesk всё же победили, тем более что её решение находится в Google за пару секунд по элементарной фразе: "NuGet CopyLocal false". Однако, как оказалось, воз и поныне там - ни одна сборка, подключенная путём установки NuGet пакета не имеет свойства CopyLocal установленного в false.

Я не верю в то, что в Autodesk работают настолько глупые люди (глупых бы не взяли на работу, как мне кажется). То, что я систематически вижу, всякий раз подтверждает моё давно сформировавшееся убеждение в том, что в Autodesk работают люди, многим из которых просто наплевать на качество своей работы (это ещё хуже, чем первый вариант)... К таким людям (ИМХО) на все 100% относится группа тестирования (во главе с Михаилом Белиловским, насколько я помню), которая выполняет свою работу (если вообще выполняет) из рук вон плохо - достаточно посмотреть на качество работы того же accoreconsole.exe. Подобным отношением к своей работе страдает и тот сотрудник Autodesk, которому поручили собирать и опубликовывать NuGet пакеты AutoCAD.NET. Можно сколько угодно с умным видом разъезжать по различным конференциям, изображая бурную радость и воодушевление в процессе разговоров о программировании в AutoCAD, но если ты не выполняешь свою прямую, первостепенную обязанность - отвечать за качество тестирования продукта, то грош цена таким разговорам, улыбкам... да и самим специалистам (ИМХО).

 Ладно, не буду более трепать себе понапрасну нервы из-за качества работы обозначенных выше персонажей (моих нервов они не стоят :) ) - давайте лучше вернёмся к их злосчастным NuGet пакетам...

Отсутствие нужного значения в свойстве CopyLocal не является единственной проблемой. Текущая реализация пакетов подразумевает, что каждый раз, запуская команду Update-Package (без опции -Version) вы тем самым будете обновлять все сборки AutoCAD, подключенные в вашем проекте, до самой последней версии AutoCAD (для которой имеется соответствующий NuGet пакет).

Однако вовсе не факт, что разработчик захочет именно такое поведение. Лично я хочу, чтобы если я в своём проекте указал использование сборок от AutoCAD 2016, то и все обновления должны происходить только в рамках интересующей меня версии AutoCAD. Мне не нужно, чтобы происходило автоматическое обновление сборок до версии AutoCAD 2017, 2018 или 2020 - не надо за меня принимать подобных решений(!!!)...

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

Для того, чтобы избежать обозначенной выше проблемы, созданной Autodesk'ом, нужно для каждой версии AutoCAD формировать (всего лишь!) уникальное имя пакета (т.е. его Id). Значения Minor и Major для версий этих пакетов можно брать либо из соответствующий значений версий AutoCAD, либо начинать с 1 (это не имеет значения, но выбрав стиль нумерации версий, следует придерживаться его и в др. пакетах во избежание путанницы).

Autodesk предоставляет NuGet пакеты только начиная с версии AutoCAD  2015. Для более поздних версий пакеты не предоставляются.

Поскольку NuGet пакеты компани Autodesk не пригодны к полноценному использованию в том виде, в котором они уже который год поставляются пользователям (ИМХО), а так же поскольку отсутствуют пакеты для интересующих меня версий AutoCAD, то я создал свои NuGet пакеты, предоставляющие AutoCAD .NET API, в которых отсутствуют обозначеные мною выше проблемы.

Чаще всего я компилирую код своих проектов под AutoCAD 2009 и 2013. Результат компиляции первого из них может затем успешно загружаться в AutoCAD 2009-2012, а второго - в AutoCAD 2013 и во все более новые версии. В виду этого, в первую очередь я создал NuGet пакеты именно для AutoCAD 2009 и 2013.

В ближайшее время я, на всякий случай, создам аналогичные NuGet пакеты для всех остальных версий AutoCAD из диапазона AutoCAD 2009-2017 (на тот случай, если мне в каком-то проекте, по какой-то причине, понадобится иная версия API).

UPD
Опубликовал NuGet пакеты для AutoCAD 2009-2017 (на каждую версию AutoCAD по три пакета).

Прокси в AutoCAD

Обновлена утилита по работе с прокси в AutoCAD.

Что нового...

1. CadProxy был переименован в Proxy Tools for AutoCAD (т.е. в Прокси-инструменты для AutoCAD).
2. Теперь это бесплатное программное обеспечение вместо открытого программного обеспечения.
3. Добавлены русская и английская лицензии.
4. Добавлена русская локализация.
5. Для AutoCAD 2009-2011: в процессе загрузки приложения в AutoCAD, родительский каталог приложения будет добавлен в Путь доступа к вспомогательным файлам (если это ещё не было сделано ранее).* Это необходио для работы справочной системы приложения.
6. Для AutoCAD 2009-2011: в реестре будет выполнена регистрация приложения (загрузка по требованию), если это не было сделано ранее.* В виду этого, вызов команды _NETLOAD для этих версий AutoCAD потребуется только один раз.
7. Операция ПРОВЕРИТЬ (_AUDIT) будет выполняться каждый раз перед расчленением или удалением прокси, если команды ВЗПРОКСИ (_XPROXY) или УДПРОКСИ (_RMPROXY) не являются предыдущей выполненной командой. Так же команда ПРОВЕРИТЬ (_AUDIT) будет автоматически выполняться и после работы этих команд.
8. Если файлы меню CUICUIX были ранее загружены пользователем в AutoCAD непосредственно из каталога расширения, тогда они будут выгружены, скопированы в Windows-профиль пользователя и затем в AutoCAD будут загружены эти копии.*
9. Изображения в файлах меню CUICUIX были заменены. Теперь эти изображения используют ICO формат вместо BMP (новые изображения используют прозрачность).
10. MNR-файлы были удалены из MSI-инсталлятора.
11. MSI-инсталлятор был полностью переписан. Теперь он показывает лицензию, устанавливаемые наборы компонентов, а так же предоставляет фиксированный набор каталогов, в которые может быть выполнена установка приложения.
12. Имеется две версии локализации MSI-установщика: английская и русская.
13. Инсталлятор добавляет пункты меню в Пуск -> Все программы. Локализация этих пунктов меню совпадает с локализацией использованного MSI-инсталлятора.
14. Теперь 32-битная версия MSI-инсталлятора не может использоваться для Windows x64 (ограничение добавлено намеренно).
15. Теперь расширение может быть установлено как с административными правами, так и без них. Это зависит от выбора, который сделает пользователь в процессе установки приложения.
16. Файл справки полностью переписан.
17. Исправлены некоторые ошибки в программном коде.


* - Проверка будет выполняться каждый раз при загрузке приложения в AutoCAD.

Пользовательское меню:



 Меню в Пуск -> Все программы:


 Дополнительная информация, размещаемая в реестре:


Возможность выбрать целевой каталог установки:


Локализованная справка:




И снова о хостинге WCF-сервисов в 2016-м accoreconsole.exe…

Подумал тут... Вполне возможно, что Autodesk умышленно прикрыла возможность хостинга сервисов в 2016-м accoreconsole.exe, дабы если не полностью пресечь (т.к. это не возможно), то хотя бы максимально усложнить возможность использования инструмента в системе распределённых приложений взаимодействующих через сервисы...

Наличие возможности хостинга WCF-сервисов в accoreconsole.exe для ряда случаев может существенно сократить потребность в количестве приобретаемых лицензий AutoCAD на предприятиях, где выполнение некоторого (если даже не всего) объёма работ можно автоматизировать (т.е. сделать программно). Одним сервисом или набором сервисов (читать как "одной лицензией AutoCAD") в таком случае сможет пользоваться неограниченное количество сотрудников компании одновременно. Для компании клиента это, безусловно, экономия... Но вот для компании Autodesk - это потеря денег... Т.е. налицо явный конфликт интересов и вполне очевидно, кто в данной ситуации имеет на руках "все козыри".

Кроме того, не стоит забывать, что теперь в Autodesk хотят одних и тех же овец стричь каждый год (новая ценовая политика) в виду чего даже сама по себе потенциальная возможность  сокращения лицензий вряд ли будет воспринята ими положительно. Т.о., как я уже писал выше: не исключено, что в 2016-м AutoCAD "кислород" сервисам был перекрыт преднамеренно...

Резюме
Я не удивлюсь, если в более новых версиях AutoCAD приложение accoreconsole.exe будет вовсе тихо изъято, мол "за ненадобностью"...

О хостинге WCF-сервисов в accoreconsole.exe (продолжение)

В продолжение предыдущей записи по обозначенной теме...

Экспериментальным путём выяснил, что проблема хостинга WCF сервисов в accoreconsole.exe присутствует в AutoCAD 2016, но отсутствует во всех более ранних версиях (2013-2015). Проверялось на AutoCAD 2016 x64 SP1 English с установленным Hotfix 3. Проверить наличие обозначенной проблемы в AutoCAD 2017 нет возможности за неимением оного, но очень даже не исключено, что обозначенный баг будет теперь и в нём и во всех последующих версиях...

Примечание:
Во всех без исключениях версиях accoreconsole.exe наблюдал ещё и такую проблему: то, что программно отправляется в консоль AutoCAD через Editor.WriteLine(...) по факту в консоли не появляется... Можно вместо этого воспользоваться Application.ShowAlertDialog(...) - в этом случае текст попадает в консоль, но это очень похоже на выдёргивание зубов плоскогубцами через зад...

О хостинге 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` - мы заставим Вас работать через задницу!

Хостинг 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);
}