Создание загрузчика плагинов

Дата публикации: 21.07.2010
Дата последнего редактирования: 12.08.2010
Состояние статьи: планируется изменение статьи - добавление уровней загрузок: уровень групп и уровень пользователей и многое др.

Предварительный обзор проблемы

Автозагрузку плагинов можно осуществлять посредством регистрации их в реестре, либо принудительно подгружать их с помощью др. ранее загруженного файла. Мне не нравится идея с регистрацией в реестре, тем более, что производя подобные настройки, я хочу чтобы все выполненные мною изменения сразу же становились доступными всем пользователям нашего домена (либо определённой группе пользователей этого домена). Это лучше выполнять через файлы настроек, которые хранятся в общедоступном для всех пользователей месте на сервере (доступ только для чтения). 

В нашей компании несколько сотен пользователей, основная масса которых работает с AutoCAD 2009, однако есть и такие, которые используют другие версии AutoCAD: 2005, 2008 и 2011. Для каждой версии программы часто требуется использовать свою версию библиотек ARX, NET ( а порой и LISP). Т.о. наш менеджер загрузки должен уметь отличать то, в какую версию AutoCAD какие библиотеки вообще потенциально могут быть загружены (грузить всё подряд, игнорируя то, что не удалось загрузить - плохое решение). 

Помимо этого библиотеки так же следует разделять и по назначению - есть общие библиотеки, которые используются всеми пользователями, а есть такие, которые специфичны для определёных групп пользователей (например дорожникам не интересны библиотеки архитекторов, равно как и архитекторам - библиотеки дорожников). Т.о. для каждой группы проектировщиков требуется свой набор библиотек, которые могут частично пересекаться с аналогичными наборами других проектных групп. Библиотеки должны храниться централизованно, в доступном для чтения месте.

Схема решения задачи

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


Детализация решения

Вначале должен загружаться некий плагин, который подобно локомотиву, будет вытягивать в загрузку все необходимые библиотеки. Содержимое общего файла сценария должно иметь следующую структуру

LoadingSettings.xml:

   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <!--Настройки, отвечающие за загрузку плагинов в различные версии САПР-->
   3:  <PlaginsLoadingSettings>
   4:    <!--Определения САПР, к которым в дальнейшем могут быть (но не обязательно) применены сценарии данного файла-->
   5:    <CADsDefinition>
   6:      <!--Количество элементов CAD может быть любым. В атрибуте 'CadName' должно указываться то значение, которое прописано для данного AutoCAD в поле 'ProductName' реестра.
   7:      Значение атрибута 'Key' должно быть уникальным для каждой записи 'CAD'.-->
   8:      <CAD Key = "Acad_2009_x86"  CadName = "AutoCAD 2009" bin = "x86" />
   9:      <CAD Key = "Acad_2011_x86"  CadName = "AutoCAD 2011" bin = "x86" />
  10:      <CAD Key = "Acad_2009_x64"  CadName = "AutoCAD 2009" bin = "x64" />
  11:      <CAD Key = "Acad_2011_x64"  CadName = "AutoCAD 2011" bin = "x64" />
  12:    </CADsDefinition>
  13:    <!--Именованные переменные, содержащие полные пути к определённым каталогам. Эти переменные могут использоваться в атрибутах различных тэгов, например в атрибуте 'FilePath' 
  14:    тэга 'LoadingList' текущего конфигурационного файла или же в тех файлах, в которых непосредственно прописаны фильтры, согласно которым осуществляется выборка загружаемых 
  15:    библиотек. 
  16:    Например, если определена именованная переменная 'myVar', хранящая значение 'C:\AcadPlagins\ARX\2009', то для того, чтобы в файлах настройки указать, к примеру, файл 
  17:    'C:\AcadPlagins\ARX\2009\x86\Arx_x86_ListLoader.xml', достаточно будет прописать такое значение: '%myVar%\x86\Arx_x86_ListLoader.xml'-->
  18:    <PathVariables>
  19:      <PathVariable VarName="var1" Value="\\Hyprostroy\dfs\SystemFolder\tools\AutoCAD tools\AcadPlagins"/>
  20:      <PathVariable VarName="var2" Value="C:\AcadPlagins"/>    
  21:    </PathVariables>
  22:    <!--Сценарии загрузок плагинов. Большинство LISP-библиотек может быть успешно загружено почти в любую версию САПР. К сожалению этого нельзя сказать о 
  23:    библиотеках ARX и .NET, поэтому каждый сценарий в блоке 'TargetCADs' должен содержать перечень версий САПР, для которых этот сценарий может быть успешно выполнен.-->
  24:    <LoadingScenaries>
  25:      <!--Сценарий для проектировщиков разделов КЖ и КМ-->
  26:      <Scenary Name = "Kj_Km" CanBeUse = "true" Description="Разделы КЖ и КМ">
  27:        <!--Версии САПР, к которым может быть применён ДАННЫЙ сценарий"-->
  28:        <TargetCADs>        
  29:            <Target CadKey = "Acad_2009_x86"/>        
  30:        </TargetCADs>
  31:        <!--Группы загрузок (группировка осуществляется по типу библиотек: ARX/LISP/.NET). Не обязательно должны присутствовать все три группы.-->
  32:        <LoadingGroups>
  33:          <!--В каждой группе (т.е. в элементе LoadingGroup) может быть сколько угодно дочерних элементов 'LoadingList'. В данном сценарии их по одному в каждой группе.-->
  34:           <LoadingGroup Name = "ARX" Load = "true" >
  35:             <!--В атрибуте 'FilePath' указывается xml-файл, в котором прописаны правила, согласно которым происходит отбор требующихся к загрузке библиотек.-->
  36:             <LoadingList FilePath = "%var1%\Arx\Arx_2009_x86_LoadingList.xml" Load = "true" />
  37:           </LoadingGroup>
  38:          <LoadingGroup Name = "LISP" Load = "true" >
  39:             <LoadingList FilePath = "%var1%\LISP\LispLoadingList.xml" Load = "true" />          
  40:           </LoadingGroup>
  41:            <LoadingGroup Name = "NET" Load = "true" >
  42:             <LoadingList FilePath = "%var1%\Net\Net_3.5_LoadingList.xml" Load = "true" />
  43:           </LoadingGroup>
  44:        </LoadingGroups>
  45:      </Scenary>    
  46:    </LoadingScenaries>
  47:  </PlaginsLoadingSettings>

В приведённом выше коде присутствует только один сценарий (элемент Scenary), хотя их можно прописать любое количество. Содержимое файла вполне понятно, дам пояснение лишь к нескольким моментам:

1. В группе PathVariables содержатся переменные, которым назначаются пути к различным каталогам. Как их использовать - указано в комментариях, но нужно всегда помнить одну деталь - в именах каталогов можно использовать имена системных переменных операционной системы! Т.е. можно указать путь "%windir%\Temp" - и это сработает, при этом такую переменную можно не объявлять в группе PathVariables. Если в группе PathVariables имеется переменная с таким же именем, что и системная переменная операционной системы - то системная переменная перекроет переменную группы PathVariables (я решил, что это должно происходить именно так).

2. Тэги LoadingList указывают на xml-файлы, в которых сосредоточена логика фильтрации нужных библиотек. Ниже приводится содержимое трёх таких файлов, используемых в приведённом выше примере
в строках 36, 39 и 42 файла LoadingSettings.xml:

Arx_2009_x86_LoadingList.xml:

   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <!--Список загружаемых плагинов-->
   3:  <LoadingFilters>
   4:    <!--Атрибут 'Directory' содержит путь к каталогу, в котором должен осуществляться поиск файлов. В имени каталога можно использовать имена переменных, которые
   5:    определены в составе файла 'LoadingSettings.xml' в блоке 'PathVariables'. Имена переменных следует заключать между двумя символами '%'. 
   6:    В атрибуте 'IncludeFilters' указываются фильтры, на основании которых производится отбор нужных библиотек. Между собой фильтры следует разделять символом ';'.
   7:    Атрибут 'ExcludeFilters' (является необязательным) содержит перечень фильтров (разделённых символом ';'), с помощью которых отфильтровываются не нужные файлы.
   8:    Атрибут 'Recursion' указывает, следует ли выполнять рекурсивный поиск по дочерним каталогам. 
   9:    Атрибут 'Use' указывает, следует ли обрабатывать данный элемент 'LoadingFilter'.-->
  10:    <LoadingFilter Directory="%var1%\ARX" IncludeFilters="*_2007x86.arx;*_2009x86.arx" ExcludeFilters="ExplodeProxy_2007x86.arx" Recursion="true" Use="true"/>
  11:  </LoadingFilters>

LispLoadingList.xml:

   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <!--Список загружаемых плагинов-->
   3:  <LoadingFilters>
   4:    <!--Атрибут 'Directory' содержит путь к каталогу, в котором должен осуществляться поиск файлов. В имени каталога можно использовать имена переменных, которые
   5:    определены в составе файла 'LoadingSettings.xml' в блоке 'PathVariables'. Имена переменных следует заключать между двумя символами '%'. 
   6:    В атрибуте 'IncludeFilters' указываются фильтры, на основании которых производится отбор нужных библиотек. Между собой фильтры следует разделять символом ';'.
   7:    Атрибут 'ExcludeFilters' (является необязательным) содержит перечень фильтров (разделённых символом ';'), с помощью которых отфильтровываются не нужные файлы.
   8:    Атрибут 'Recursion' указывает, следует ли выполнять рекурсивный поиск по дочерним каталогам. 
   9:    Атрибут 'Use' указывает, следует ли обрабатывать данный элемент 'LoadingFilter'.-->
  10:    <LoadingFilter Directory="%var1%\LISP" IncludeFilters="*.lsp;*.fas;*.vlx" ExcludeFilters="Acad*.lsp;Acad*.fas" Recursion="true" Use="true"/>    
  11:  </LoadingFilters>

Net_3.5_LoadingList.xml:

   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <!--Список загружаемых плагинов-->
   3:  <LoadingFilters>
   4:    <!--Атрибут 'Directory' содержит путь к каталогу, в котором должен осуществляться поиск файлов. В имени каталога можно использовать имена переменных, которые
   5:    определены в составе файла 'LoadingSettings.xml' в блоке 'PathVariables'. Имена переменных следует заключать между двумя символами '%'. 
   6:    В атрибуте 'IncludeFilters' указываются фильтры, на основании которых производится отбор нужных библиотек. Между собой фильтры следует разделять символом ';'.
   7:    Атрибут 'ExcludeFilters' (является необязательным) содержит перечень фильтров (разделённых символом ';'), с помощью которых отфильтровываются не нужные файлы.
   8:    Атрибут 'Recursion' указывает, следует ли выполнять рекурсивный поиск по дочерним каталогам. 
   9:    Атрибут 'Use' указывает, следует ли обрабатывать данный элемент 'LoadingFilter'.-->
  10:    <LoadingFilter Directory="%var1%\Net\Net 3.5 x86" IncludeFilters="*.dll" Recursion="true" Use="true"/>
  11:  </LoadingFilters>

3. Логика фильтрации заключена в тэгах LoadingFilter. Таких тэгов может быть сколько угодно, хотя в примере присутствует только один.

Создаю файл LocalSettings.xml, в котором я буду сохранять различные локальные настройки для всех своих плагинов, а так же информацию о том, какой сценарий загрузки для какой версии AutoCAD, установленного на данной машине, следует запускать при старте САПР. В первой версии моего загрузчика этот файл будет общим для всех и располагаться в том же каталоге, где и сам загрузчик, поскольку данная версия плагина ориентирована пока только на управление загрузками для всего домена в целом, без учёта индивидуальных настроек загрузок для групп и отдельных пользователей (это будет реализовано в следующей версии). Содержимое указанного файла таково:

   1:  <?xml version="1.0" encoding="utf-8" ?> 
   2:  <!--Локальные настройки пользователя-->
   3:  <LocalSettings>
   4:    <Scenaries>
   5:      <!--Текущий сценарий запуска плагинов для AutoCAD 2009 x86-->
   6:      <CurrentScenary CadKey="Acad_2009_x86" ScenaryName="Kj_Km"/>
   7:    </Scenaries>  
   8:  </LocalSettings>

Существует несколько типов библиотек, которые можно загружать в AutoCAD:
  1. ARX
  2. LISP
  3. .NET
  4. VBA (от этого типа библиотек Autodesk уже отказался, потому в дальнейшем его не учитываю).
Для каждого типа используется свой способ программной загрузки: 
  1. класс Autodesk.AutoCAD.Interop.AcadApplication имеет два экземплярных метода - LoadArx и LoadDVB, с помощью которых можно загружать библиотеки ARX и VBA. Однако, как это ни странно, метода LoadLisp в природе нет, хотя LISP появился в AutoCAD задолго до VBA и уже успел его пережить... Для загрузки lsp и fas файлов будем использовать статический метод ads_queueexpr (SendStringToExecute к сожалению использовать не удастся) класса Autodesk.AutoCAD.ApplicationServices.Document.
  2. Для загрузки .Net плагинов мы будем использовать экземплярный метод Load класса System.AppDomain.
Далее опубликовываю исходный код решения:

Сначала создаю структуру, в которой буду размещать информацию об установленных на локальной машине версиях AutoCAD.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.Collections;
   6:   
   7:  namespace Bushman.Autodesk.AutoCAD
   8:  {
   9:      /// <summary>
  10:      /// Структура, в которую упаковываю информацию, извлечённую из реестра, о версии AutoCAD.
  11:      /// </summary>
  12:      public struct AcadInfo :IEqualityComparer<AcadInfo>
  13:      {
  14:          public string Location { get; set; }
  15:          public string Language { get; set; }
  16:          public string ProductName { get; set; }
  17:          public string PatchTitle { get; set; }
  18:          public string Release { get; set; }
  19:          public string Bin { get; set; }
  20:          public string Tag { get; set; }
  21:   
  22:          public bool Equals(AcadInfo x, AcadInfo y)
  23:          {
  24:              return x.ProductName.Trim().ToUpper() == y.ProductName.Trim().ToUpper() && x.Bin.Trim().ToUpper() == y.Bin.Trim().ToUpper();
  25:          }
  26:   
  27:          public int GetHashCode(AcadInfo obj)
  28:          {
  29:              return obj.GetHashCode();
  30:          }
  31:      }
  32:  }


Код класса-загрузчика:

   1:  using System;
   2:  using System.Collections;
   3:  using System.Collections.Generic;
   4:  using System.Linq;
   5:  using System.Xml.Linq;
   6:  using System.Text;
   7:  using System.Reflection;
   8:  using System.IO;
   9:  using Microsoft.Win32;
  10:  //Autodesk
  11:  using acad = Autodesk.AutoCAD.ApplicationServices.Application;
  12:  using Autodesk.AutoCAD.ApplicationServices;
  13:  using Autodesk.AutoCAD.DatabaseServices;
  14:  using Autodesk.AutoCAD.EditorInput;
  15:  using Autodesk.AutoCAD.Runtime;
  16:  using Autodesk.AutoCAD.Interop;
  17:  using System.Threading;
  18:  using System.Runtime.InteropServices;
  19:   
  20:  namespace Bushman.Autodesk.AutoCAD
  21:  {
  22:      /// <summary>
  23:      /// Тяговая лошадка, вытягивающая в загрузку все библиотеки, выбранные согласно правилам, указанным в активном сценарии загрузки
  24:      /// </summary>
  25:      public sealed class AcadPlaginsLoader : IExtensionApplication
  26:      {
  27:          Document dwg;
  28:          Database db;
  29:          Editor ed;
  30:          XElement xLoadingSettings = null;
  31:          XElement xLocalSettings = null;
  32:          string loadingSettingsFileName;
  33:          string usersSettingsFileName;
  34:          Dictionary<string, string> vars;
  35:          Dictionary<string, List<string>> filesForLoading;
  36:   
  37:          public void Initialize()
  38:          {
  39:              //Начальная инициализация переменных
  40:              //-------------- 
  41:              dwg = acad.DocumentManager.MdiActiveDocument;
  42:              db = dwg.Database;
  43:              ed = dwg.Editor;
  44:   
  45:              //Загрузив единожды ARX или .NET библиотеки, ими могут пользоваться все документы в рамках текущего сеанса. Однако с LISP-файлами дело обстоит иначе -
  46:              //их функции загружаются применительно к конкретному чертежу. Следовательно при открытии очередного чертежа, должна происходить очередная загрузка всех
  47:              //LISP-файлов. Для этого производим регистрацию события открытия чертежа.
  48:              acad.DocumentManager.DocumentCreated += new DocumentCollectionEventHandler(DocumentManager_DocumentCreated);
  49:              //Получаю полный путь к каталогу, в котором располагается библиотека текущего плагина
  50:              string currentDir = this.GetType().GetCurrentDirectory();//Расширяющий метод
  51:              
  52:              //Формирую полные имена файлов
  53:              string lds = "LoadingSettings.xml";
  54:              loadingSettingsFileName = Path.Combine(currentDir, lds);
  55:              string lcs = "LocalSettings.xml";
  56:              //Сначала в usersSettingsFileName сохраняю ссылку на стартовую резервную копию файла локальных настроек
  57:              usersSettingsFileName = Path.Combine(currentDir, lcs);
  58:   
  59:              //Проверяю файлы на предмет их наличия
  60:              if (!new FileInfo(loadingSettingsFileName).Exists) throw new FileNotFoundException("Файл не найден", lds);
  61:              if (!new FileInfo(usersSettingsFileName).Exists) throw new FileNotFoundException("Файл не найден", lcs);
  62:   
  63:              //Локальные настройки пользователя будем хранить в профиле пользователя, в специально отведённом для этих целей каталоге
  64:              string appDir = "MyCadSettings";
  65:              string lad = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), appDir);
  66:              DirectoryInfo xdir;
  67:              //Если каталога ещё нет - создаём его
  68:              if (!new DirectoryInfo(lad).Exists)
  69:              {
  70:                  xdir = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)).CreateSubdirectory(appDir);
  71:              }
  72:              else
  73:              {
  74:                  xdir = new DirectoryInfo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), appDir));
  75:              }
  76:              FileInfo fls = new FileInfo(usersSettingsFileName);
  77:              //Если в каталоге профиля отсутствует файл настроек - создаём этот файл
  78:              if (!new FileInfo(Path.Combine(xdir.FullName, fls.Name)).Exists)
  79:              {
  80:                  fls = fls.CopyTo(Path.Combine(xdir.FullName, fls.Name), true);
  81:              }
  82:              fls = new FileInfo(Path.Combine(xdir.FullName, fls.Name));
  83:              usersSettingsFileName = fls.FullName;
  84:              //--------------
  85:              //Считываю данные файла LocalSettings.xml (в нём находятся различные локальные настройки, а так же имя сценария, 
  86:              //который должен запускаться для текущей версии AutoCAD).
  87:              xLocalSettings = ReadSettingsToXElement(usersSettingsFileName);
  88:   
  89:              //Считываю данные файла LoadingSettings.xml (в нём находятся сценарии загрузок плагинов).
  90:              xLoadingSettings = ReadSettingsToXElement(loadingSettingsFileName);
  91:   
  92:              try
  93:              {
  94:                  //Загружаю необходимые плагины
  95:                  LoadPlagins();
  96:              }
  97:              catch (System.Exception e)
  98:              {
  99:                  ed.WriteMessage("\n" + e.Message + "\n");
 100:              }
 101:          }
 102:   
 103:          //Данный метод запускается при открытии каждого очередного чертежа
 104:          void DocumentManager_DocumentCreated(object sender, DocumentCollectionEventArgs e)
 105:          {
 106:              dwg = acad.DocumentManager.MdiActiveDocument;
 107:              db = dwg.Database;
 108:              ed = dwg.Editor;
 109:              ed.WriteMessage("\nПроизошло открытие чертежа\n");
 110:   
 111:              StringBuilder sbOk = new StringBuilder();
 112:              StringBuilder sbLispErr = new StringBuilder();
 113:              StringBuilder sbLispErrHead = new StringBuilder();
 114:   
 115:              sbOk.Append("\n\n");
 116:              sbOk.Append(new string('*', 30));
 117:              sbOk.Append("\nЗагрузка LISP-файлов:\n");
 118:              sbOk.Append(new string('*', 30));
 119:   
 120:              sbLispErrHead.Append("\n\n");
 121:              sbLispErrHead.Append(new string('*', 30));
 122:              sbLispErrHead.Append("\nНе удалось загрузить следующие LISP-файлы:\n");
 123:              sbLispErrHead.Append(new string('*', 30));
 124:   
 125:              foreach (string file in filesForLoading["LISP"])
 126:              {
 127:                  try
 128:                  {
 129:                      AcadPlaginsLoader.ads_queueexpr(string.Format("(load \"{0}\" \"\")\n", file.Replace("\\", "\\\\")));
 130:                      sbOk.Append(string.Format("\n'{0}'", file));
 131:                  }
 132:                  catch (System.Exception ex)
 133:                  {
 134:                      sbLispErr.Append(string.Format("\nОшибка: '{1}'; Файл: '{0}'.", file, ex.Message));
 135:                  }
 136:              }
 137:              sbOk.Append("\n");
 138:              ed.WriteMessage(sbOk.ToString());
 139:              if (sbLispErr.Length != 0) sbLispErr = sbLispErrHead.Append(sbLispErr.ToString());
 140:              ed.WriteMessage(sbLispErr.ToString());
 141:          }
 142:   
 143:          /// <summary>
 144:          /// Считывание данных из xml-файла
 145:          /// </summary>
 146:          /// <param name="settingsFileName">Имя xml-файла, содержащего данные</param>
 147:          /// <returns>Возвращается экземпляр класса XElement, в котором упакованы считанные данные.</returns>
 148:          private XElement ReadSettingsToXElement(string settingsFileName)
 149:          {
 150:              try
 151:              {
 152:                  return XElement.Load(settingsFileName);
 153:              }
 154:              catch (System.Exception e)
 155:              {
 156:                  ed.WriteMessage("\n" + e.Message + "\n");
 157:                  throw e;
 158:              }            
 159:          }
 160:   
 161:          public void Terminate()
 162:          {
 163:              ed.WriteMessage(Localization.LocalizationResources.TerminateMessage);            
 164:          }
 165:   
 166:          /// <summary>
 167:          /// Загрузка плагинов, согласно настройкам сценария, указанного к использованию в файле LocalSettings.xml для текущей версии AutoCAD
 168:          /// </summary>
 169:          private void LoadPlagins()
 170:          {
 171:              //DynamicLinker.LoadApp - арх
 172:              //AppDomain.Load - net
 173:   
 174:              //Определяю версию AutoCAD текущей сессии. Для этого получаю полное имя каталога, в котором хранится acad.exe текущей сессии, после чего
 175:              //Извлекаю из реестра записи обо всех установленных на компьютере версиях AutoCAD, отфильтровываю информацию, оставляя только ту, которая
 176:              //соответствует текущей сессии acad.exe
 177:              AcadApplication app = (AcadApplication)acad.AcadApplication;
 178:              string acadDir = app.Path;
 179:   
 180:              //Версия AutoCAD текущей сессии
 181:              AcadInfo currentCad = GetInstalledAcads().Where(n => n.Location.Trim().ToUpper() == acadDir.Trim().ToUpper()).First();
 182:   
 183:              //Получаю имя ключа, назначенного для данной версии AutoCAD в файле настроек сценариве
 184:              string cadKey = GetCadKey(currentCad);
 185:   
 186:              if (cadKey == string.Empty || cadKey == null)
 187:              {
 188:                  string errMsg = "Не удалось получить значение ключа AutoCAD из файла настройки сценариев";
 189:                  ed.WriteMessage(string.Format("\n{0}\n",errMsg));
 190:                  throw new System.Exception(errMsg);
 191:              }
 192:   
 193:              //Из локальных настроек пользователя извлекаю имя текущего сценария, назначенного для запущенной в рамках данной сессии версии AutoCAD
 194:              string scenaryName = xLocalSettings.Element("Scenaries").Elements("CurrentScenary").Where(n => n.Attribute("CadKey").Value.Trim().ToUpper() == cadKey)
 195:                  .Select(n => n.Attribute("ScenaryName").Value.Trim().ToUpper()).DefaultIfEmpty().First();
 196:              
 197:              //Извлекаю содержимое сценария
 198:              XElement scenary = xLoadingSettings.Element("LoadingScenaries").Elements("Scenary").Where(n => Boolean.Parse(n.Attribute("CanBeUse").Value) && n.Attribute("Name").Value.Trim().ToUpper() == scenaryName)
 199:                  .DefaultIfEmpty().First();
 200:              //Проверяю, что сценарий может быть запущен в текущей версии AutoCAD
 201:              if (scenary.Element("TargetCADs").Elements("Target").Select(n => n.Attribute("CadKey").Value.Trim().ToUpper()).Where(n => n == cadKey).Count() == 0)
 202:                  throw new System.Exception("Указанный сценарий не может быть запущен в текущей версии AutoCAD, т.к. он отсутствует в перечне блока 'TargetCADs'");
 203:              
 204:              //Извлекаю имена всех переменных, определённых в файле настроек
 205:              
 206:              //В этих списках будет храниться перечень библиотек, которые следует загрузить.
 207:              List<FileInfo> arxList = new List<FileInfo>();
 208:              List<FileInfo> lispList = new List<FileInfo>();
 209:              List<FileInfo> netList = new List<FileInfo>();
 210:              vars = new Dictionary<string, string>();
 211:              xLoadingSettings.Element("PathVariables").Elements("PathVariable").Select(n =>
 212:              {
 213:                  vars.Add(string.Format("%{0}%", n.Attribute("VarName").Value.Trim().ToUpper()),
 214:                  n.Attribute("Value").Value); return n;}).ToArray();
 215:   
 216:              //В этом словаре группирую (по типу библиотек) имена файлов, являющихся списками загрузок (т.е. непосредственно в этих файлах определены правила фильтрации).
 217:              Dictionary<string, List<string>> result = new Dictionary<string, List<string>>();
 218:              //Обрабатываю все имеющиеся группы
 219:              foreach (XElement group in scenary.Element("LoadingGroups").Elements("LoadingGroup").Where(n => Boolean.Parse(n.Attribute("Load").Value)))
 220:              {
 221:                  string[] files = group.Elements("LoadingList").Where(n => Boolean.Parse(n.Attribute("Load").Value)).Select(n => n.Attribute("FilePath").Value).ToArray();
 222:                  string groupName = group.Attribute("Name").Value;
 223:                  List<string> subResult = new List<string>();
 224:                  result.Add(groupName, subResult);
 225:                  foreach (string file in files)
 226:                  {
 227:                      subResult.Add(GetFileNameWithoutVariables(vars, file));                
 228:                  }
 229:              }
 230:              //Теперь необходимо получить сгруппированный (по типу библиотек) список файлов, которые подлежат загрузке в AutoCAD, в рамках текущего сценария.
 231:              filesForLoading = new Dictionary<string, List<string>>();
 232:              foreach (KeyValuePair<string,List<string>> item in result)
 233:              {
 234:                  List<string> files = new List<string>();
 235:                  filesForLoading.Add(item.Key, files);
 236:   
 237:                  foreach (string xFile in item.Value)
 238:                  {
 239:                      if (! new FileInfo(xFile).Exists)
 240:                      {
 241:                          ed.WriteMessage(string.Format("\nФайл '{0}' (список фильтрации) не найден.\n", xFile));
 242:                      }
 243:                      else
 244:                      {
 245:                          IEnumerable<XElement> xes = XElement.Load(xFile).Elements("LoadingFilter").Where( n => Boolean.Parse(n.Attribute("Use").Value));
 246:                          foreach (XElement xe in xes)
 247:                          {
 248:                              //Здесь нужно обработать каждый элемент списка
 249:                              bool recurcy = Boolean.Parse(xe.Attribute("Recursion").Value);
 250:                              string directory = GetFileNameWithoutVariables(vars, xe.Attribute("Directory").Value);
 251:                              string filters = xe.Attribute("IncludeFilters").Value;
 252:                              string noFilters = xe.Attribute("ExcludeFilters") == null ? string.Empty : xe.Attribute("ExcludeFilters").Value;
 253:   
 254:                              string[] includeFiles = GetFilteredFiles(directory, filters, recurcy).Select(n => n.FullName).ToArray();
 255:                              string[] excludeFiles = GetFilteredFiles(directory, noFilters, recurcy).Select(n => n.FullName).ToArray();
 256:                              //Перечень объектов, подлежащих загрузке. Данный перечень получен на основании информации, указанной в файле списков фильтрации
 257:                              string[] resultFiles = includeFiles.Except(excludeFiles).ToArray();
 258:                              //Добавляю результат в общий список файлов, подлежащих загрузке
 259:                              files.AddRange(resultFiles);
 260:                          }
 261:                      }
 262:                  }
 263:                  //При использовании нескольких списков фильтрации, возможна вероятность дубляжа информации. Устраняем дубляж:
 264:                  files = files.Distinct().ToList();
 265:                  //Отчёт о загрузке вывожу на консоль AutoCAD, группируя информацию по типу загружаемых библиотек. Помимо этого разделяю информацию на
 266:                  //успешную и неуспешную загрузку
 267:                  StringBuilder sbOk = new StringBuilder();
 268:                  StringBuilder sbNetErr = new StringBuilder();
 269:                  StringBuilder sbNetErrHead = new StringBuilder();
 270:                  StringBuilder sbArxErr = new StringBuilder();
 271:                  StringBuilder sbArxErrHead = new StringBuilder();
 272:                  StringBuilder sbLispErr = new StringBuilder();
 273:                  StringBuilder sbLispErrHead = new StringBuilder();
 274:                  switch (item.Key)
 275:                  {
 276:                      case "NET":
 277:                          sbOk.Append("\n\n");
 278:                          sbOk.Append(new string('*', 30));
 279:                          sbOk.Append("\nУспешная загрузка .Net-библиотек:\n");
 280:                          sbOk.Append(new string('*', 30));
 281:   
 282:                          sbNetErrHead.Append("\n\n");
 283:                          sbNetErrHead.Append(new string('*', 30));
 284:                          sbNetErrHead.Append("\nНеудалось загрузить следующие .Net-библиотеки:\n");
 285:                          sbNetErrHead.Append(new string('*', 30));
 286:                          //AppDomain domain = AppDomain.CurrentDomain;
 287:                          foreach (string  file in files)
 288:                          {
 289:                              try
 290:                              {
 291:                                  Assembly asm = Assembly.LoadFrom(file);
 292:                                  //domain.Load(asm.FullName);
 293:                                  sbOk.Append(string.Format("\n'{0}'", file));
 294:                              }
 295:                              catch (System.Exception e)
 296:                              {
 297:                                  sbNetErr.Append(string.Format("\nОшибка: '{1}'; Файл: '{0}'.", file, e.Message));
 298:                              }
 299:                          }
 300:                          break;
 301:                      case "ARX":
 302:                          sbOk.Append("\n\n");
 303:                          sbOk.Append(new string('*', 30));
 304:                          sbOk.Append("\nУспешная загрузка ARX-библиотек:\n");
 305:                          sbOk.Append(new string('*', 30));
 306:   
 307:                          sbArxErrHead.Append("\n\n");
 308:                          sbArxErrHead.Append(new string('*', 30));
 309:                          sbArxErrHead.Append("\nНе удалось загрузить следующие ARX-библиотеки:\n");
 310:                          sbArxErrHead.Append(new string('*', 30));
 311:                          foreach (string  file in files)
 312:                          {
 313:                              try
 314:                              {
 315:                                  ((AcadApplication)acad.AcadApplication).LoadArx(file);
 316:                                  sbOk.Append(string.Format("\nУспешно загружен: '{0}'", file));
 317:                              }
 318:                              catch (System.Exception e)
 319:                              {
 320:                                  sbArxErr.Append(string.Format("\nОшибка: '{1}'; Файл: '{0}'.", file, e.Message));
 321:                              }
 322:                          }
 323:                          break;
 324:                      case "LISP":
 325:                          sbOk.Append("\n\n");
 326:                          sbOk.Append(new string('*', 30));
 327:                          sbOk.Append("\nЗагрузка LISP-файлов:\n");
 328:                          sbOk.Append(new string('*', 30));
 329:   
 330:                          sbLispErrHead.Append("\n\n");
 331:                          sbLispErrHead.Append(new string('*', 30));
 332:                          sbLispErrHead.Append("\nНе удалось загрузить следующие LISP-файлы:\n");
 333:                          sbLispErrHead.Append(new string('*', 30));
 334:                          foreach (string  file in files)
 335:                          {
 336:                              try
 337:                              {
 338:                                  AcadPlaginsLoader.ads_queueexpr(string.Format("(load \"{0}\" \"\")\n", file.Replace("\\", "\\\\")));
 339:                                  sbOk.Append(string.Format("\n'{0}'", file));
 340:                              }
 341:                              catch (System.Exception e)
 342:                              {
 343:                                  sbLispErr.Append(string.Format("\nОшибка: '{1}'; Файл: '{0}'.", file, e.Message));
 344:                              }
 345:                          }
 346:                          break;                    
 347:                      default:
 348:                          throw new System.Exception("Не допустимое значение наименования группы. Допустимы следующие наименования: ARX, LISP, NET");                        
 349:                  }
 350:                  sbOk.Append("\n");
 351:                  ed.WriteMessage(sbOk.ToString());
 352:                  if (sbNetErr.Length != 0) sbNetErr = sbNetErrHead.Append(sbNetErr.ToString());
 353:                  ed.WriteMessage(sbNetErr.ToString());
 354:                  if (sbArxErr.Length != 0) sbArxErr = sbArxErrHead.Append(sbArxErr.ToString());
 355:                  ed.WriteMessage(sbArxErr.ToString());
 356:                  if (sbLispErr.Length != 0) sbLispErr = sbLispErrHead.Append(sbLispErr.ToString());
 357:                  ed.WriteMessage(sbLispErr.ToString());
 358:              } 
 359:          }
 360:   
 361:          /// <summary>
 362:          /// Метод заменяет имена переменных, использующихся в имени каталога, на реальные значения этих переменных
 363:          /// </summary>
 364:          /// <param name="vars">Словарь, содержащий перечень имеющихся переменных</param>
 365:          /// <param name="file">Имя файла, содержащее в своём составе имя переменной</param>
 366:          /// <returns>Возвращается строковое значение полного имени файла</returns>
 367:          private string GetFileNameWithoutVariables(Dictionary<string, string> vars, string file)
 368:          {
 369:              string x = file;
 370:              if (x.Where(n => n == '%').Count() == 0)
 371:              {
 372:   
 373:              }
 374:              else if ((x.Where(n => n == '%').Count() & 1) == 0)
 375:              {
 376:                  int start = 0;
 377:                  int end = 0;
 378:                  while (x.Contains('%'))
 379:                  {
 380:                      start = x.IndexOf('%');
 381:                      end = file.IndexOf('%', start + 1);
 382:                      string key = x.Substring(start, end + 1 - start);
 383:                      string keyU = key.Trim().ToUpper();
 384:                      //Если имеется системная переменная с таким же именем, то назначаем ей более высокий приоритет - будет браться значение из неё.
 385:                      string value = Environment.GetEnvironmentVariable(keyU.Replace("%", string.Empty));
 386:                      if (value == null)
 387:                      {
 388:                          value = vars[keyU];
 389:                      }
 390:                      value = value.Last() == '\\' ? value.Substring(0, value.Length - 1) : value;
 391:                      x = file.Replace(key, keyU).Replace(keyU, value);
 392:                  }
 393:              }
 394:              else
 395:              {
 396:                  string errMsg = "В строке пути должно быть чётное количество символов '%', или не быть совсем";
 397:                  throw new System.Exception(errMsg);
 398:              }
 399:              return x;
 400:          }
 401:   
 402:          /// <summary>
 403:          /// Получение отфильтрованного списка файлов
 404:          /// </summary>
 405:          /// <param name="path">Каталог, относительно которого должен осуществляться поиск</param>
 406:          /// <param name="filters">Фильтры, по которым должна осуществляться выборка. Между собой фильтры следует разделять символом ';'</param>
 407:          /// <param name="recurcy">Выполнять ли рекурсивный поиск по подкаталогам. True - выполнять, False - не выполнять</param>
 408:          /// <returns>Возвращается результат, упакованный в экземпляры классов FileInfo.</returns>
 409:          private IEnumerable<FileInfo> GetFilteredFiles(string path, string filters, bool recurcy)
 410:          {
 411:              DirectoryInfo dir = new DirectoryInfo(path);
 412:              if (!dir.Exists) throw new DirectoryNotFoundException(string.Format("Каталог '{0}' не найден.", dir.FullName)); 
 413:   
 414:              List<FileInfo> filteredFiles = new List<FileInfo>();
 415:              foreach (string filter in filters.Split(';').Where(n => n != string.Empty))
 416:              {
 417:                  SearchOption searchOption = recurcy ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
 418:                  filteredFiles.AddRange(dir.GetFiles(filter, searchOption));
 419:              }
 420:              return filteredFiles;
 421:          }
 422:   
 423:          private string GetCadKey(AcadInfo currentCad)
 424:          {
 425:              return xLoadingSettings.Element("CADsDefinition").Elements("CAD").Select(n => new AcadInfo(){ProductName = n.Attribute("CadName").Value, 
 426:                  Bin = n.Attribute("bin").Value, Tag = n.Attribute("Key").Value}).Where(n => n.Equals(n, currentCad)).DefaultIfEmpty().First().Tag.Trim().ToUpper();
 427:          }
 428:   
 429:          /// <summary>
 430:          /// Получение перечня установленных версий AutoCAD
 431:          /// </summary>
 432:          /// <returns>Возвращается AcadInfo[]</returns>
 433:          private AcadInfo[] GetInstalledAcads()
 434:          {
 435:              string acadNodeName = @"SOFTWARE\Autodesk\AutoCAD";            
 436:              string wow6432 = @"SOFTWARE\Wow6432Node\Autodesk\AutoCAD";            
 437:              string bin = Registry.LocalMachine.OpenSubKey(wow6432) != null ? "x64" : "x86";
 438:   
 439:              //Если операционная система является x86 с установленным AutoCAD x86, или же если операционная система x64 с установленным AutoCAD x64
 440:              RegistryKey acadNode = Registry.LocalMachine.OpenSubKey(acadNodeName);
 441:   
 442:              //*********************
 443:              if (acadNode == null)
 444:              {
 445:                  ed.WriteMessage("\nНа данном компьютере не обнаружено установленных версий AutoCAD" + "\n");
 446:              }
 447:   
 448:              AcadInfo[] acads_1 = GetInstalledAcads(acadNode, bin);
 449:              AcadInfo[] acads_2 = null;
 450:              if (bin == "x64")
 451:              {
 452:                  acads_2 = GetInstalledAcads(Registry.LocalMachine.OpenSubKey(wow6432), bin);
 453:              }
 454:              //*********************
 455:              if (acads_2 != null)
 456:                  return acads_1.Cast<AcadInfo>().Concat(acads_2.Cast<AcadInfo>()).ToArray();
 457:              return acads_1;
 458:          }
 459:   
 460:          /// <summary>
 461:          /// Получить информацию об установленных версиях AutoCAD
 462:          /// </summary>
 463:          /// <param name="acadNode">Ветвь реестра, подлежащая обработке</param>
 464:          /// <param name="bin">разрядность системы</param>
 465:          /// <returns>Результат упаковывается в массив объектов AcadInfo</returns>
 466:          private AcadInfo[] GetInstalledAcads(RegistryKey acadNode, string bin)
 467:          {
 468:              string acadSpNodeName = @"Service Packs";
 469:              List<AcadInfo> result = new List<AcadInfo>();
 470:              IEnumerable<RegistryKey> acads = acadNode.GetSubKeyNames().SelectMany(n => acadNode.OpenSubKey(n).GetSubKeyNames().Select(m => acadNode.OpenSubKey(Path.Combine(n, m))));
 471:              foreach (RegistryKey cad in acads)
 472:              {
 473:                  AcadInfo acad = new AcadInfo();
 474:                  acad.ProductName = (string)cad.GetValue("ProductName", string.Empty);
 475:                  acad.Language = (string)cad.GetValue("Language", string.Empty);
 476:                  acad.Release = (string)cad.GetValue("Release", string.Empty);
 477:                  acad.Location = (string)cad.GetValue("Location", string.Empty);
 478:                  acad.Bin = bin;
 479:                  RegistryKey spParentNode = cad.OpenSubKey(acadSpNodeName);
 480:                  if (spParentNode != null && spParentNode.GetSubKeyNames().Length > 0)
 481:                  {
 482:                      RegistryKey spNode = spParentNode.OpenSubKey(spParentNode.GetSubKeyNames()[0]);
 483:                      acad.PatchTitle = (string)spNode.GetValue("PatchTitle", string.Empty);
 484:                      acad.Release = (string)spNode.GetValue("Release", string.Empty);
 485:                  }
 486:                  result.Add(acad);
 487:              }
 488:              return result.ToArray();
 489:          }
 490:   
 491:          /// <summary>
 492:          /// Метод имитирует работу консоли AutoCAD и выполняется только после того, как чертёж будет полностью инициализирован.
 493:          /// Данный метод взят мною отсюда: http://through-the-interface.typepad.com/through_the_interface/2006/08/calling_command.html
 494:          /// </summary>
 495:          /// <param name="strExpr">Строковое выражение, которое должно быть передано в консоль AutoCAD</param>
 496:          /// <returns></returns>
 497:          [DllImport("acad.exe", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
 498:          extern static private int ads_queueexpr(string strExpr);
 499:      }
 500:  }

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

Лично я предпочитаю не лезть в реестр, а инициализировать загрузку своего "ослика", тянущего свою повозку - с помощью такого lsp-файла:

Acad.lsp:

   1:  ;Инициализация менеджера загрузок...
   2:  (defun-q HwdStartup()
   3:  (c:PlaginsManager)
   4:  )
   5:  ;;;Подгружаю .Net библиотеку...
   6:  (defun c:PlaginsManager()
   7:  (setq echo (getvar "cmdecho"))
   8:  (setvar "cmdecho" 1)
   9:  (setq fd (getvar "filedia"))
  10:  (setvar "filedia" 0)
  11:  (command "_netload" "\\\\Hyprostroy/dfs/SystemFolder/tools/AutoCAD tools/AcadPlagins/PlaginsLoader/PlaginsManager.dll")
  12:  (setvar "filedia" fd)
  13:  (setvar "cmdecho" echo)
  14:  )
  15:  ;;;Исключаю возможность перезаписи метода S::STARTUP...
  16:  (setq S::STARTUP (append S::STARTUP HwdStartup))

В каталогах поиска профиля AutoCAD добавляю первой строчкой запись, указывающую на тот каталог, в котором находится приведённый мною выше lsp-файл. 

На компьютерах пользователей следует запустить команду (один раз на каждом компьютере, дабы прописать нужные разрешения в настройках платформы), которая позволит выполнять использование удалённо расположенных .Net-библиотек. Для этого в командной строке Windows (т.е. из cmd.exe), запускаю С ПРАВАМИ АДМИНИСТРАТОРА это:
"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CasPol.exe" -m -ag 1.2 -url "file://\\hyprostroy/dfs/SystemFolder/tools/AutoCAD tools/AcadPlagins/*" FullTrust 

Данная строка даёт полное доверие ко всем .net-библиотекам, размещённым в указанном мною в качестве одного из параметров каталоге (и во всех его подкаталогах). Дополнительную информацию на тему предоставления прав для загрузки удалённых библиотек, можно почитать здесь.

Всё - теперь при каждом запуске AutoCAD, у пользователей будут подгружаться все необходимые библиотеки (определённые для общего использования в составе домена):


Фрагмент консольного вывода, получаемого в результате работы плагина:




Исходный код проекта (MS Visual Studio 2010) здесь.

Откомпилированные версии библиотеки:
  1. x86 Release
  2. x64 Release
Для работы требуется, чтобы на компьютере была установлена библиотека .Net Framework 3.5 SP1, библиотека тестировалась на AutoCAD 2009 x86.
Comments