Модераторы: Partizan, gambit
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Многопоточный конструктор плагинов на C#, Почитайте статью, помогите ее подправить 
:(
    Опции темы
Necias
Дата 19.7.2006, 20:37 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 45
Регистрация: 7.4.2006

Репутация: 2
Всего: 2



Многопоточный конструктор плагинов
на C#.
    Меня всегда поражала гибкость, которую предоставляет программе хороший конструктор плагинов. Наиболее известным примером, ИМХО, является Miranda IM – фантастический пример программы, полностью являющейся конструктором. Опираясь на исходный код этого мультипротокольного клиента я попытался разработать простенький многопоточный движок на совремнном – но таком высокоуровневом - языке C# (пользуясь Visual Studio 2005, и, соответственно, .NET Framework 2.0).
    Поскольку эта задача является очень комплексной и конечный код больше напоминает змея Уробороса – то есть не имеет ни конца, ни начала, – придется выкладывать огромные куски хорошо комментированного кода. 
    К сожалению мои скромные преподавательские способности не позволяют объяснить эту тему совершенно неподготовленному человеку. Рекомендую осваивать эту статью, разобравшись с отражениями, потоками, а также архитектурой любой – самой-самой-самой простенькой операционки ибо конструктор плагинов, по сути аналогичен ОС.
    Определимся с понятием плагина – это просто dll-библиотека, содержащая класс с заранее заданным именем и несколькими стандартными функциями, способный обрабатывать возникающие события и (не обязательно) поддерживающий некоторые сервисы. Сервис – это просто такая функция, соответствующая некоторому шаблону и вызываемая по мере необходимости (аналогией сервиса в Windows являются WinAPI функции. Продолжая аналогию, событиям в нашем конструкторе синонимичны… события в windows))))
    Начнем с конструктора. Этот класс во многом опирается на класс PluginClass, в котором, собственно, и происходит самое интересное, однако здесь осуществляются важные функции – добавляются в очередь событий новые события, вызываются сервисы
Код

    public  class Constructor
    {
        //массив плагинов
        private  PluginClass[] plgArray = new PluginClass[10];
        //число уже загруженных плагинов
        private  ushort PluginsLoaded;

        //Очередь событий
        public  EventDescription[] ArrEvents = new EventDescription[20];
        //Номер нового события в очереди
        private  ushort CurrentEvent;

        public Constructor()
        {
            //номер нового события - 0
            CurrentEvent = 0;
            //0-е событие - пустое. На нем прервется обработка очереди
            ArrEvents[0].EventName = "ZeroNonFunctioningEvent";
            //число загруженных плагинов = 0
            PluginsLoaded = 0;
        }

        //Обрабатываем вызовы сервисов основной программы
        //в Миранде каждый сервис должен был зарегистрировать свою функцию т.е. указать
        //свое имя и адрес функции, который впоследствии находился по этому имени
        //здесь все реализовано проще - одной-единственной функции передается имя желаемого
        //сервиса и его параметры. Если такое имя не обрабатывается... ну... возвращаем ашипку
        //в виде строки
        private  object ServiceParser(string Service, object HParam, object LParam)
        {
            //просто для примера, как это делается
            if (Service == "msg")
            {
                Console.WriteLine((string)HParam);
                return null;
            }
            //строка с ашипкой
            return "Can't find service: " + Service;
        }

        /*Жутко зубодробильная функция! Добавляет новое событие в очередь
        *Здесь прищлось ввести понятие "нулевого события" - событие с именем
        *ZeroNonFunctioningEvent является сигналом для остановки обработки очереди
        *(таким сигналом, в принципе, можно выставить просто пустую строку)
        *Это работает примерно так: очередь состоит из n элементов. мы знаем, что у нас в ней уже есть
        *k событий. При возникновении нового события, мы записываем его на позицию k + 1
        * а на позицию k + 2 ставим нулевое событие. Если очередь переполняется, идем по второй
        */
        public void RaiseEvent(string EventName, object HPARAM, object LPARAM)
        {
            //Если это нулевое событие или пустая строка - вылетаем, иначе будут баги
            if ((EventName == "") || (String.Compare(EventName, "ZeroNonFunctioningEvent") == 0)) { return; }
            //Добавляем событие в очередь
            //К имени события прибавляем имя потока. Если это осн. поток - ниче не изменится
            //а если это поток какогото плага - гарантирует уникальность имени события
            //(см. InitPlugin)
            ArrEvents[CurrentEvent].EventName = Thread.CurrentThread.Name + EventName;
            ArrEvents[CurrentEvent].HPARAM = HPARAM;
            ArrEvents[CurrentEvent].LPARAM = LPARAM;
            CurrentEvent++;
            //Если очередь переполнена - переходим к нулевой позиции и проходим ее по новому кругу
            if (CurrentEvent == 20) { CurrentEvent = 0; }
            //следующее после добавленного событие должно быть т.н. нулевым
            ArrEvents[CurrentEvent].EventName = "ZeroNonFunctioningEvent";
        }

        /*Еще одна сложная функция, вызывающая ф-ию, обрабатывающую вызовы сервисных функций
         *у какого-либо плагина (извините, я не нарочно такую фразу написал). Работает так:
         * требует указать имя плагина PluginName, в котором вызывается сервис ServiceName
         * Мы этот плагин ищем в массиве, вызываем функцию ServiceCallback, которой и передаем
         * имя сервиса и параметры
         */
        public object CallService(string PluginName, string ServiceName, object HParam, object LParam)
        {
            //Если у плагина нет имени, значит, это запрос к основной программе
            if (PluginName == "")
            {
                return ServiceParser(ServiceName, HParam, LParam);
            }
            else
            {
                //Ищем плагин с указанным именем
                for (ushort i = 0; i < PluginsLoaded; i++)
                {
                    //Имя плагина = имя потока
                    if (String.Compare(plgArray[i].thrdThis.Name, PluginName) == 0)
                    {
                        try
                        {
                            //параметры передаются через массив object'ов
                            //если интересно более подробно - почитайте о классе MethodInfo
                            object[] parametrs = new object[3];
                            parametrs[0] = ServiceName;
                            parametrs[1] = HParam;
                            parametrs[2] = LParam;
                            //Пытаемся вызвать указанный сервис
                            plgArray[i].ServiceCallback.Invoke(plgArray[i].MainObj, parametrs);
                        }
                        catch (Exception e)
                        {
                            return "Error in module " + PluginName + ": " + e.ToString();
                        }
                    }
                }
            }
            return "Can't find module: " + PluginName;
        }
        /*
         * Функция, инициализирующая плагин по его имени
         */
        public  bool InitPlugin(string PlugFile)
        {
            //инициализируем новый элемент в массиве plgArray
            plgArray[PluginsLoaded] = new PluginClass();
            //записываем имя dll в параметр PluginFile
            //т.к. мы не сможем передать его в функцию plgArray[].InitNewPlugin
            plgArray[PluginsLoaded].PluginFile = PlugFile;
            //инициализируем переменную потока нового плагина
            plgArray[PluginsLoaded].thrdThis = new Thread(plgArray[PluginsLoaded].InitNewPlugin);
            //Имя потока = имя файла dll
            plgArray[PluginsLoaded].thrdThis.Name = PlugFile.Substring(PlugFile.LastIndexOf('\\') + 1, PlugFile.LastIndexOf('.') - PlugFile.LastIndexOf('\\') - 1);
            //Поехали!
            plgArray[PluginsLoaded].thrdThis.Start();
            //Здесь мы ждем, когда плагин загрузится
            //Конечно, можно реализовать это "ожидание" при помощи семафоров или монитора
            //но я старался упростить
            //Ждем, когда флаг загрузки станет 1 или 2
            while (true)
            {
                //если флаг = 2 - плагин успешно загрузился
                if (plgArray[PluginsLoaded].FunctionFlag == 2) { PluginsLoaded++; goto m1; };
                //Если = 1 - что-то не так
                if (plgArray[PluginsLoaded].FunctionFlag == 1) {goto m1; }
                //Не грузим зазря проц, поспим немного
                Thread.Sleep(10);
            }
        m1: ;
            //Пример вызова нового события
            RaiseEvent("NewModuleLoaded", (object)PlugFile, null);
            return true;
        }
    }

    Поговорим о классе Plugin, который осуществляет работу непосредственно с некоторым dll-файлом плагина.
    Что нам нужно, чтобы работать с плагином? Во-первых его надо загрузить. Для загрузки плагина, являющегося dll-сборкой, достаточно воспользоваться статической функцией класса System.Reflection.Assembly LoadFile в самом простом ее варианте, требующим в аргументе строку – путь к файлу сборки. Функция возвращает результат типа System.Reflection.Assembly – класс для работы с загруженной сборкой.
    Теперь нам каким-то образом необходимо извлечь тип, определенный в сборке. Поскольку тип этот должен во-первых иметь специальное имя, напрмиер, SPlug (т.е. мы сможем его найти!) и во-вторых должен определять несколько стандартных функций (чтобы организовать связь!). Воспользуемся тем, что мы можем получить из загруженной нами сборки объект ManifestModule – как ясно из названия, это модуль манифеста)). Соответственно поскольку у нас есть манифест, мы можем достать из него нужный нам тип! А когда мы достанем нужный тип мы можем создать экземпляр класса SPlug, а также получить из него потребные нам методы. Информация о методах содержится в класса MethodInfo, для вызова метода нужно вызвать функцию Invoke соответствующего экземпляра класса MethodInfo и передать ей параметры для метода (параметры просто записываются по порядку в массив object’ов) и экземпляр класса, в котором будет выполняться метод (если вы не поняли, не переживайте – в исходном коде все станет ясно).
    Затем мы обрабатываем события, находящиеся в очереди событий. Формирование очереди вы увидите при рассмотрении класса Constructor. Вот код для класса Plugin
Код

    class PluginClass
    {
        //Сборка dll-плагина
        private Assembly asmThis;
        //в эту переменную будет загружен основной класс с установленным именем, находящийся в dll плагина
        private Type StartupType;
        //Это для обработчика событий - номер обрабатываемого события
        private ushort CurEvent;
        //Поток этого плагина
        public Thread thrdThis;
        //имя файла плагина
        public string PluginFile;
        //в Эту переменную будет загружен основной класс с установленным именем, находящийся в dll плагина
        public object MainObj;
        //флаг загрузки плагина 0 = загружается, 1 - ошибка при загрузке, 2 - загрузка успешна
        public ushort FunctionFlag;
        //эта функция вызывается для обработки события
        public MethodInfo EventCallback;
        //Эта функция вызывается для запроса сервиса
        public MethodInfo ServiceCallback;

        public void InitNewPlugin()
        {
            object[] MoveParams = new object[1];
            //Флаг обращается в 0 - загружаем плагин
            FunctionFlag = 0;
            //Инициируем плагин
            try
            {
                //Загружаем сборку. Я использую LoadFrom, хотя есть и другая, идентичная ей
                //функция LoadFile, но у них есть существенные различия (LoadFrom принимает 
                //и относительный и абсолютный путь к файлу, и не проверяет при загрузке сборки,
                //нет ли другой такой же - в отличии от LoadFile)
                asmThis = Assembly.LoadFrom(PluginFile);
                //Находим нужный нам тип. ПОМНИТЕ: в имя типа входит и имя пространства имен
                StartupType = asmThis.ManifestModule.GetType("SPlug", true, true);
                //формируем массив object'ов для передачи конструктору параметров
                object[] param = new object[3];
                //передаем адреса функций CallService и RaiseEvent
            /*Это просто эпохальный момент! Здесь скрывается огромный потенциал: теперь плагин может вызвать сервис и не только из главной программы, но и из другого плагина более того – плагин может возбуждать события. Теперь мы можем совсем извратиться и писать плагин к плагину*/
                param[0] = Program.k.GetType().GetMethod("CallService");
                param[1] = Program.k.GetType().GetMethod("RaiseEvent");
                //Передаем Текущий Конструктор - необходимо указывать его при вызове методов (см. плагин)
                param[2] = (object)Program.k;
                //Загружаем объект
                MainObj = Activator.CreateInstance(StartupType, param);
                //Берем адреса функций
                EventCallback = MainObj.GetType().GetMethod("EventCallback");
                ServiceCallback = MainObj.GetType().GetMethod("ServiceCallback");
            }
            catch (Exception e)
            {
                //Загрузка плагина не удалась, флаг обращается в 1
                FunctionFlag = 1;
                return;
            }
            //Загрузка завершена успешно, флаг обращается в 2
            FunctionFlag = 2;
            //обработчик событий
            //Текущее событие = 0
            CurEvent = 0;
            //Infinite loop :)
            while (true)
            {
                //если CurEvent не нулевое событие, обрабатываем
                if (String.Compare(Program.k.ArrEvents[CurEvent].EventName, "ZeroNonFunctioningEvent") != 0)
                {
                    //формируем массив параметров для вызова ф-ии EventCallback в плагине
                    object[] parametrs = new object[3];
                    parametrs[0] = Program.k.ArrEvents[CurEvent].EventName;
                    parametrs[1] = Program.k.ArrEvents[CurEvent].HPARAM;
                    parametrs[2] = Program.k.ArrEvents[CurEvent].LPARAM;
                    //вызываем обработчик внутри плагина
                    EventCallback.Invoke(MainObj, parametrs);
                    //Если событие - CloseProgram, выходим из потока после обработки
                    if (String.Compare("CloseProgram", Program.k.ArrEvents[CurEvent].EventName) == 0)
                    {
                        goto ExitPoint;
                    }
                    //переходим к следующему событию
                    CurEvent++;
                    if (CurEvent == 20) { CurEvent = 0; }
                }
                else
                {
                    //если CurEvent - нулевое событие, то спим немного
                    Thread.Sleep(50);
                }
            }
        ExitPoint: ;
        }
    }
 
Фуф! Вот собственно и все. Очень рекомендую протрэйсить пример – это чрезвычайно простенькая консольная программа, там же – пример плагина.
Замечания: вся вышеописанная громоздкая конструкция не претендует на оптимальность (хотя я и старался), безопасность от неперехваченных исключений при полномасштабном использовании и т.д. Однако основная идея соответствует выбранной теме, а исполнение (более или менее) – духу .NET. Обратите внимание: полность безопасный код, никаких указателей! 
Использовались источники: Г. Шилдт, «полный справочник по C#», MSDN, идеи и приемы из исходного кода MirandaIM.
(с) Necias aka Kergan aka Хабибуллин Т., [email protected]
   

Это сообщение отредактировал(а) Necias - 19.7.2006, 20:41

Присоединённый файл ( Кол-во скачиваний: 6 )
Присоединённый файл  Constructor.rar 23,18 Kb
PM MAIL ICQ   Вверх
mr.DUDA
Дата 19.7.2006, 21:36 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


3D-маньяк
****


Профиль
Группа: Экс. модератор
Сообщений: 8244
Регистрация: 27.7.2003
Где: город-герой Минск

Репутация: 110
Всего: 232



Necias, честно говоря что-то тут непонятно, а точнее непонятно, для чего это всё нужно, и почему должно быть нужно, если всё так непонятно зачем это всё нужно smile

Если нужно (тьфу!)... если необходимо расширить программу плагинами, то обычно (точнее не так)... то некоторые люди объявляют такую штуку как интерфейс, выносят туда плагиновские методы, свойства и события... Дальше просто делаем обычную DLL ака Class Library с одним или несколькими классами, реализующими наш интерфейс... Далее в основной программе перечисляют assembly с типами, реализующими данный интерфейс (ака загрузка плугинов)... Ну и наконец - главная прога подписывается на события и дёргает методы, приводящие плугины в действие. Всё, и не нужны нам флаги и прочие замечательные вещи smile 


--------------------
user posted image
PM MAIL WWW   Вверх
Necias
Дата 19.7.2006, 22:51 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 45
Регистрация: 7.4.2006

Репутация: 2
Всего: 2



если говорить о наследовании интерфейсов - то да, это один из вариантов передачи информации о стандартных функциях. Но при этом не надо забывать, что интерфейс должен быть объявлен в сторонней библиотеке универсальных типов, иначе ничем воспользоваться не удастся по крайней мере при динамическом использовании - даже если интерфейс в плагине будет таким же как и в главной программе. Здесь я просто избежал введения дополнительной длл, работая с отражениями. "подписывается на события" в одном треде, да еще если события получает ТОЛЬКО "главная прога" это действительно элементарно, но тут мультитред + события возбуждаются кем угодно и без очереди событий будет у тебя нещастье (когда событий будет много, начнут пропадать). А как "дергать методы", но не из главной проги, а из плагина, использующего функционал другого плагина? При условии, что  оба должны использовать один экземпляр класса. Так и появляется концепция сервисных функций (т.е. Ф-ий, не предусмотренных в интерфейсах, но которые созданы для использования другими плагинами). Замечание о наследовании интерфейсов верное, однако здесь, как уже было сказано, реализован другой подход - ручная загрузка методов (ненамного более трудоемкий), других же проблем это наследование, как видите, не решает. Следовало заранее сказать: это не просто конструктор, расширяющий функционал - это инструмент module-independent реализации приложения (в литературе я встречал термин "модулярный двигатель").  если, например, сравнивать конструктор и язык программирования, то обычные конструкторы - это процедурно-модульные языки, а модулярный - это уже объектно-ориентированный(заметьте, ориентированный, а не обоснованный). Например если мы хотим добавить работу с какой нить хитрой бд - добавляем модуль. Но это может и обычный конструктор. Здесь же этот модуль смогут использовать другие плагины, причем - напрямую. Например, плагин ОТЧЕТОВ. а дальше? Пишем плагин бизнес-анализа с хитрыми формулами и длинными алгоритмами. И она использует плагин отчетов. Угу? 
PM MAIL ICQ   Вверх
arilou
Дата 19.7.2006, 22:58 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Великий МунаБудвин
****


Профиль
Группа: Экс. модератор
Сообщений: 2646
Регистрация: 15.7.2004
Где: город-герой Минск

Репутация: 21
Всего: 61



Necias, рекомендую почитать вот тут:  smile 
http://forum.vingrad.ru/index.php?showtopic=37869

Добавлено @ 23:12 
Necias, 
Цитата(Necias @  19.7.2006,  22:51 Найти цитируемый пост)
Но при этом не надо забывать, что интерфейс должен быть объявлен в сторонней библиотеке универсальных типов, иначе ничем воспользоваться не удастся по крайней мере при динамическом использовании - даже если интерфейс в плагине будет таким же как и в главной программе

Для чего создавать универсальный механизм плагинов? В каждой программе свои потребности, а one-size-fits-all - это не самый лучший выход.

Цитата(Necias @  19.7.2006,  22:51 Найти цитируемый пост)
 как "дергать методы", но не из главной проги, а из плагина, использующего функционал другого плагина?

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

P.S.
Цитата(Necias @  19.7.2006,  20:37 Найти цитируемый пост)
 любой – самой-самой-самой простенькой операционки ибо конструктор плагинов, по сути аналогичен ОС

упростили немного ситуацию  smile 
 


--------------------
user posted imageuser posted image
PM WWW ICQ   Вверх
Necias
Дата 19.7.2006, 23:52 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 45
Регистрация: 7.4.2006

Репутация: 2
Всего: 2



а почему бы не сделать универсальным и "ковергентным" то, что можно сделать таковым? За счет функции CallService это элементарно и нет разницы, используется ли функция из интерфейса или она вызывается как сервис.
По поводу вставки референции и получении инстанса - каюсь, не подумал smile 
PM MAIL ICQ   Вверх
arilou
Дата 20.7.2006, 00:11 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Великий МунаБудвин
****


Профиль
Группа: Экс. модератор
Сообщений: 2646
Регистрация: 15.7.2004
Где: город-герой Минск

Репутация: 21
Всего: 61



Цитата(Necias @  19.7.2006,  23:52 Найти цитируемый пост)
 почему бы не сделать универсальным и "ковергентным" то, что можно сделать таковым? За счет функции CallService это элементарно и нет разницы, используется ли функция из интерфейса или она вызывается как сервис.

Это будет позднее связывание, и ошибки, которые вполне могли быть проверены на этапе компиляции, будут перенесены в рантайм, что не есть хорошо. 


--------------------
user posted imageuser posted image
PM WWW ICQ   Вверх
mr.DUDA
Дата 20.7.2006, 10:37 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


3D-маньяк
****


Профиль
Группа: Экс. модератор
Сообщений: 8244
Регистрация: 27.7.2003
Где: город-герой Минск

Репутация: 110
Всего: 232



Наверное, это дело пристрастия к определённому стилю проектирования. Кому-то нравится так, кому-то сяк, а кое-кто сделает наперекосяк... Ну это лирика. По теме хочется сказать, что модульное проектирование в приведённой трактовке ничем не отличается от компонентно-ориентированного, те же eggs только сбоку.

Заводя ещё одну длл с описанием интерфейсов, мы ничего не потеряем, а если так уж не хочется - пожалуйста, добавляйте в плагин референс на основную программу, где описан интерфейс.

По поводу многопоточности конечно сложно что-то с уверенностью сказать. Единственное, это

Цитата(Necias @  19.7.2006,  22:51 Найти цитируемый пост)
работая с отражениями. "подписывается на события" в одном треде, да еще если события получает ТОЛЬКО "главная прога" это действительно элементарно, но тут мультитред + события возбуждаются кем угодно и без очереди событий будет у тебя нещастье (когда событий будет много, начнут пропадать).

Куда это будут события "пропадать" ? Генерация события C# суть обычный вызов метода. В том же потоке. Если и будут проблемы с многопоточностью - то в другой плоскости, когда понадобится доступ к одним ресурсам из нескольких потоков, появятся lock-и и т.п. 


--------------------
user posted image
PM MAIL WWW   Вверх
Necias
Дата 20.7.2006, 14:48 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 45
Регистрация: 7.4.2006

Репутация: 2
Всего: 2



Сейчас объясню, про очередь. Допустим ее у нас нет и мы сразу, из функции raiseevent вызываем нужный метод. А что это значит? Это значит, что мы должны войти в поток каждого плагина иначе вызванный метод будет работать в главном потоке и вызвать нужный метод. При этом основной поток должен оставаться в функции raiseevent, тоесть мы должны ждать, когда обработается событие. Такие вхождения обходятся крайне дорого по времени, а при неграмотной реализации семафоров есть риск потери событий некоторыми потоками. Когда этот конструктор в версии без очереди событий тестировался на эффексть, тормоза были жуткие и от мультитреда - одно название. Очередь повысила производительность весьма ощутимо. 
PM MAIL ICQ   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Прежде чем создать тему, посмотрите сюда:
mr.DUDA
THandle

Используйте теги [code=csharp][/code] для подсветки кода. Используйтe чекбокс "транслит" если у Вас нет русских шрифтов.
Что делать если Вам помогли, но отблагодарить помощника плюсом в репутацию Вы не можете(не хватает сообщений)? Пишите сюда, или отправляйте репорт. Поставим :)
Так же не забывайте отмечать свой вопрос решенным, если он таковым является :)


Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, mr.DUDA, THandle.

 
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Общие вопросы по .NET и C# | Следующая тема »


 




[ Время генерации скрипта: 0.0527 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.