Журнал LinuxFormat - перейти на главную

LXF95:Mono

Материал из Linuxformat
Перейти к: навигация, поиск
    Едва вы начинаете делать что-нибудь интересное, XML сразу же становится медленным и громоздким, и Пол Хадсон думает, что пора взяться за работу с базами данных...

Содержание

Mono-Мания

    Программирование на современной платформе для новичков Mono-Мания Программирование на современной платформе для новичков Поскольку это — последний номер, который попадет к редакто- ру Ребекке, скажу не таясь: пишет она уж больно мелко. Когда мы возвращаемся из отпуска, на наших столах нас ждут ков- ры из желтых листков-наклеек для заметок, покрытых неразборчивыми каракулями. На вид сущая грязь, пока вы не вооружитесь лупой и не раз- глядите ее аккуратный почерк. Конечно, потом начинается надсадная для глаз работа – читать все эти записки, а ведь можно было обойтись всего лишь SQL-запросом, в стиле SELECT Значимое FROM ЗапискиРебекки Наш эксперт WHERE Приоритетность!=Не относится к делу. Увы, все попытки заставить Ребекку взаимодействовать через базу данных потерпели крах, и теперь она ищет другую работу. Но вы-то все еще здесь, верно? Значит, у меня есть время, чтобы подсадить вас на SQL, прежде чем вы заскучаете от Mono и перейдете к разделу Hardcore Linux!
    На этом уроке мы напишем небольшой инструмент, способный следить за всеми Mono-программами, которые вы создали на протяжении всей нашей серии. Каждые десять секунд программа будет опрашивать Mono о списке всех активных приложений, а затем сохранять этот список в базе данных. Мы получим шанс изучить два новых аспекта Mono: чтение информации о процессах и доступ к базе данных. Этот проект – еще одна звездочка вам на погоны! Как и для всех наших Monoпроектов, потребуется среда исполнения .NET 2.0; убедитесь, что вы ее включили, через диалог Project > Options > Runtime Options.

Чтение процессов

    Начните новый консольный проект в MonoDevelop и дайте ему имя Monocular. Первым делом надо прочесть список всех процессов, работающих в Mono, и вывести информацию на экран. Решить эту задачу довольно просто, благодаря пространству имен System.Diagnostics, которое дает доступ к россыпям внутренней системной информации. Нас интересует класс Process, позволяющий читать информацию о запущенной программе. Помните, что «программа» – это исполняемый код на диске (например, Vim), а «процесс» – экземпляр программы, который в данный момент работает. То есть если вы запустите Vim шесть раз, в оперативной памяти будет шесть процессов Vim, но программ от этого не прибавится.
    Добавьте using System.Diagnostics в начале файла Main.cs, затем замените стандартную строку Hello World на следующее:


Process[] proclist = Process.GetProcesses();
foreach(Process proc in proclist) {
Console.WriteLine(proc.ProcessName);
} 

    Для работы этого достаточно, поэтому нажимайте F5 и смотрите на вывод панели Application Output. Вы увидите что-то вроде 'Monocular, MonoDevelop, mdhost, mdhost, MonoDevelop…', а также все прочие запущенные Mono-программы – Beagle, Tomboy, F-Spot и так далее. Если у вас сравнительно новая версия MonoDevelop (Fedora Core 6 или выше), будет доступно автоматическое завершение кода – вы это заметите, если нажмете . (точку) после proc: появится обширный набор свойств, которые можно узнать о процессе. Как именно поддерживаются эти свойства, зависит от версии Mono – полюбопытствуйте, что предоставляет ваша!
    Внимание: поддержка процессов улучшается с каждым релизом Mono – последние дистрибутивы по умолчанию включают Mono 1.2.3, а моя версия, из Fedora Core 6, уже приотстала.

Знамение времени

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

while(1){
if (прошло достаточно времени){
делай_работу();
}
} 

    ...но это капитальная глупость: ваше приложение будет тупо съедать процессорное время без особой пользы. Гораздо лучшее решение – применить таймер: он позволит вашему приложению уснуть на заданное время, а затем вызовет определенный вами метод.
    Метод, который мы будем использовать, называется TimesUp(). Чтобы мы смогли подключить его к таймеру, он должен принимать определенный список параметров. Создайте следующий метод:

static void TimesUp(object sender, ElapsedEventArgs e){

} 

    Теперь перенесите код из Main() в TimesUp(), потому что мы хотим вызывать его, когда наш таймер сработает. В начале файла напишите следующие строки using:

using System.Threading;
using System.Timers; 

    Чтобы ваша программа вызывала код каждые 10 секунд, создайте новый таймер и укажите ему метод TimesUp():

System.Timers.Timer timer = new System.Timers.Timer();
timer.Interval = 10000;
timer.Enabled = true;
timer.Elapsed += new ElapsedEventHandler(TimesUp); 

    Не пытайтесь запустить код в MonoDevelop! Когда вы работаете с потоками, а с таймером так дело и обстоит, MonoDevelop обычно некорректно останавливает программу в режиме отладки; поэтому лучше запустить ее из консоли.
    При запуске не произойдет никакого вывода на экран. С чего бы это? Мы возвращаемся к проблеме с потоками, рассмотренной в прошлый раз: программа достигает конца Main() и завершает работу до того, как таймер сработает. Решение состоит в приказании главному потоку (он-то и исполняет Main()) уснуть, посредством метода Thread. Sleep(). Метод позволяет задать число миллисекунд, на которые поток впадет в спячку, или же указать Timeout.Infinite, если вы хотите, чтобы поток уснул навеки. Последняя опция нам и нужна – пусть главный поток спит, а таймер будет вкалывать. В конце Main() добавьте строку:

Thread.Sleep(Timeout.Infinite); 

    Теперь при запуске программы она будет честно раз в 10 секунд выводить список процессов. Прежде чем продолжать, сделаем еще коечто: добавьте такую строку перед вызовом Thread.Sleep():

TimesUp(null, null); 

    Предоставляю вашему воображению – и тестированию! – догадаться, что она делает.

Создание базы данных в Mono

    Информацию о процессах мы получили; надо ее куда-то положить. Базы данных бывают двух типов: клиент-серверные, когда данные хранятся где-то на сервере; и встроенные, когда есть локальный файл, напрямую управляемый из программы. Клиент-серверных баз данных полным-полно – MySQL, PostgreSQL, Oracle и другие соревнуются за место под солнцем. Но среди встроенных, одна из баз заслуживает большего внимания – это SQLite. SQLite хороша по нескольким причинам: ее код является достоянием общественности и вы можете делать с ним все, что хотите; у нее есть продвинутые возможности – поддержка Unicode, транзакций и, конечно, SQL-запросов; она также очень быстрая. Используя SQLite в Mono, можно считывать и записывать файл базы данных прямо из C#.
    Слишком хорошо для этого мира? Да нет, загвоздка имеется: с базами данных не так легко работать через Mono. Если вы следовали инструкциям по установке в нашем первом выпуске (LXF87-LXF88), то уже разрешили зависимости Mono для SQLite; если нет, то я включил DLL-файл на диск, и вы можете его скопировать в папку с программой. В любом случае надо сделать ссылку на Mono.Data.SqliteClient. Есть небольшая разница между версией, входящей в Mono, и DLL – я постарался ее минимизировать, но если вы используете DLL, то вам понадобятся большие буквы в некоторых именах классов (т.е. SQLiteConnection вместо SqliteConnection).
    В любом случае, SQLite использует для хранения данных обычный файл, так что первым шагом будет его создание. Добавьте следующие строки в начало вашего файла:

using Mono.Data.SqliteClient;
using System.IO; 

    Если вы используете DLL и обнаружили, что Mono.Data.SqliteClient не существует, попытаетесь набрать Mono.Data. и выбрать верное название сборки из предложенного списка опций.
    Создание базы данных осуществляется с помощью метода File. Create(), но фактически базу надо создать только один раз – если файл уже существует, незачем пересоздавать его. Поэтому предусмотрим проверку: первый раз запускается программа или нет, а также заведем место для хранения информации о соединении с нашей базой данных SQLite. Добавьте следующие две строки под class Monocular {:

static bool FirstTime = false;
static SqliteConnection Conn; 

    Теперь подправим Main(), чтобы он автоматически создавал базу при первом запуске приложения, а затем спокойно к ней подсоединялся:

if (!File.Exists("monocular.db"))}
Firsttime=true;
File.Create("monocular.db");
}
Conn =new SqliteConnection("URL=file:monocular.db");
Conn.Open(); 

    Итак, файл для хранения ваших данных создан. Чтобы SQLite могла писать в него данные, надо еще создать таблицы, определяющие сохраняемые поля. Все команды SQLite выполняются через объекты SqliteCommand. Новая непонятка? А вы продолжайте читать. Эти SqliteCommand-объекты уникальны в Mono: несмотря на потуги программистов – разработчиков .NET-коннектора, они не убирают за собой. Если в Mono вы выделяете память под объект, то вам не надо заботиться о ее освобождении – сборщик мусора Mono автоматически сделает это за вас. SQLite иногда выделяет память, которая впоследствии автоматически не освобождается, и вам придется помнить об этом самим. В данный момент вам надо создать объект типа команда, настроив его таким образом, чтоб он создавал таблицу для хранения информации о процессах; затем выполнить команду. Вот как это выглядит на С#:

SqliteCommand dbcmd = Conn.CreateCommand();
if (FirstTime) { 
dbcmd.CommandText = "CREATE TABLE processes
(DateStamp TEXT, Name TEXT, Id INTEGER);";
dbcmd.ExecuteNonQuery();
} 

    Если вы изучали SQL раньше, то удивитесь, почему поля отмечены как TEXT и INTEGER, а не более научно, как VARCHAR(255). Дело в том, что SQLite не содержит таких четких определений. Поля хранятся либо как числа, либо как текст. Если же вы не изучали SQL, то кусок кода выше создает таблицу для хранения полей DateStamp, Name и Id – мы будем использовать их для хранения даты каждого чтения, имени запущенного процесса и его идентификатора.
    Вообще-то ExecuteNonQuery() – не самое умное имя: Q в аббревиатуре SQL означает Query – Запрос, то есть каждое предложение SQL является запросом. Ну, а ExecuteNonQuery() означает, что вы хотите заставить SQLite сделать что-то, но результаты запроса вас не интересуют.
    После создания таблицы необходимо очистить память, выделенную командой, следующим образом:

dbcmd.Dispose(); dbcmd=null; 

    Вы, возможно, удивитесь, почему эти две строки кода не входят в блок if – а потому, что мы попозже добавим еще код! В любом случае, хотя код компилируется и запускается, проку от него мало – ничего интересного он не делает.

Время писать!

    ОК, у вас есть таймер, у вас есть список процессов, а теперь еще и доступ к базе данных – налицо весь инструментарий для создания всего проекта. Единственное, чего мы не умеем –писать в базу данных, но тут имеются осложнения. Противно то, что дело это нехитрое, но в наш отдел писем недавно поступила жалоба на мой «непонятный» стиль изложения. А я решил привести оптимальный способ создания записей в базе данных – оптимальный с точки зрения производительности, а не с точки зрения обучения. Кто не согласен, пишите мне, и мы сменим акценты. Так, отвел душу – и хватит; вернемся к базе данных! Есть две вещи, которые необходимо знать, прежде чем приступать к записи в базу SQLite: 1 SQLite использует транзакции для обеспечения целостности данных. То есть, когда вы пишете строку данных, то запускается транзакция, записываются данные, и транзакция завершается. Если вдруг ваша машина откажет во время записи транзакции, то SQLite ее откатит. Это очень хорошо. 2 SQLite кэширует запросы для ускорения работы. Первый пункт очень важен, так как мы собираемся писать за раз сразу несколько строк. Если у вас работает 10 Mono-приложений, будем писать 10 строк – по одной для каждой программы. Процесс в SQLite будет выглядеть следующим образом:
    1.    Начать транзакцию
    2.    Записать строку
    3.    Закончить строку
    4.    Начать транзакцию
    5.    Записать строку
    6.    Закончить транзакцию… и так далее.


    Это очень медленно, но необходимо, так как SQLite требует, чтобы каждая команда записи была частью транзакции. Той же цели можно достичь, если велеть SQLite начать транзакцию, записать все строки сразу же, затем закончить транзакцию самим:

1 2 3 4 5 6 Начать транзакцию 
Записать строку 
Записать строку 
Записать строку 
... 
Закончить транзакцию. 

    Такой способ работает много быстрее, и он также более надежен: если наш компьютер откажет (или же другое несчастье прервет работу SQLite), то у нас не будет недоделанных записей – у нас будут либо все записи, либо ничего.
    Второй пункт важен потому, что подобные запросы очень просто писать:

for (int i = 0; i < 10; i++){
$query = "SELECT Foo FROM Bar WHERE Baz = "+i; ... { 

    Выполнится 10 различных запросов к SQLite. Лучше использовать приготовленные заранее запросы и сообщать SQLite максимум информации о запросе, помечая заменяемые части вопросительными знаками. Из примера все будет ясно, поэтому углубимся в код. Пока что TimesUp() у нас распечатывал информацию; а надо заточить его на запись в базу данных. Вот как TimesUp() должен выглядеть:

static void TimesUp(object sender, ElapsedEventArgs e) {
Process[] proclist = Process.GetProcesses();
SqliteTransaction transaction = (SqliteTransaction)Conn.
BeginTransaction();
dbcmd = Conn.CreateCommand();
dbcmd.CommandText = "INSERT INTO processes VALUES (?,?, ?);";
SqliteParameter date = new SqliteParameter();
SqliteParameter name = new SqliteParameter();
SqliteParameter id = new SqliteParameter();
date.Value = DateTime.Now.ToLongTimeString();
dbcmd.Parameters.Add(date);
dbcmd.Parameters.Add(name);
dbcmd.Parameters.Add(id);
foreach(Process proc in proclist) { 

    Это еще не вся функция, но я хочу притормозить и взглянуть на подготовленные запросы в действии. Запрос, который мы используем, передает вместо знаков вопроса информацию о процессе, так как только эта часть будет изменяться. Однако параметр date изменяется только один раз за время выполнения TimesUp(), поэтому его можно назначить сразу. Другие параметры просто оставляются как пустые объекты SqliteParameter, чтобы потом заполнять их отдельно для каждого процесса. Знаю, что это тривиально, но ради ясности ситуации скажу: необходимо вызывать dbcmd.Paramters.Add() для каждого параметра SQL-запроса, в порядке их появления в запросе.
    Ладно, продолжим с того места, на котором остановились...

foreach(Process proc in proclist) {
name.Value = proc.processName;
id.Value = proc.Id;
dbcmd.ExecuteNonQuery();
}
dbcmd.Dispose();
dbcmd = null;
transaction.Commit(); } 

    Действительно, все, что меняется между вызовами ExecuteNonQuery() – это значения двух подставляемых параметров, поэтому SQLite может использовать кэшированные запросы для получения наивысшей производительности. Последнее выражение в коде особенно важно: пока транзакция не зафиксирована, никаких записей внесено не будет – не забудьте вызывать Commit(), если, конечно, вы не любитель охоты за странными ошибками! Написано тут много, но смотреть все равно не на что, кроме как на медленное разрастание файла monocular.db. Но скоро все пойдет по-другому...

К чтению готовы!

    Последнее, что нам предстоит сделать – это считать всю информацию из базы данных, так что вернемся к методу Main() и проделаем коекакие изменения. Если пользователь запускает Monocular без параметров, то она будет бесконечно работать в фоновом режиме, считывая информацию каждые 10 секунд и записывая ее в базу данных. Но если ей предоставить параметр – любой параметр – Monocular будет выводить всю хранимую информацию. На DVD я поместил код, позволяющий выполнять сортировку по имени процессов или по идентификатору, но ради экономии места мы пока проигнорируем его значение. Сейчас нам важен параметр как таковой — он будет сигнализировать о том, что пользователю нужен режим чтения.


    В отличие от предыдущих запросов, теперь вам действительно потребуются данные – результаты SQL-запроса; добудем их с помощью специального объекта SQLiteDataReader:

string currenttime = "";
// this loops over all the rows returned by the query
while (reader.Read()) {
string datestamp = reader.GetString(0);
string name = reader.GetString(1);
int id = reader.GetInt32(2);
// we'll put some more code in here shortly
Console.WriteLine(" {0}\1{1}\2", id, name);
}
reader.Close(); 
reader = null; } 

    GetString() и GetInt32() оба принимают один параметр: какое поле вы хотите считать. То есть GetString(0) считывает первое поле (DateStamp), GetString(1) считывает второе поле, и так далее. Как и с командами, вам надо на всякий случай освободить память, выделенную для считывателей, так как SQLite может этого не сделать. Данный код выводит хранимую в таблице информацию обо всех процессах, но игнорирует поле даты. Это потому, что я хочу внести немного разнообразия: буду выводить каждый временной штамп при его изменении, тогда наш вывод будет разделяться чем-то вроде заголовков. Вы заметили пустую переменную currenttime. В нее мы занесем дату первой строки, считываемой из таблицы. Если в последующей строке окажется отличное от currenttime время, то выведется новый заголовок для нового времени, и это значение запишется в currenttime. На C# получится следующее:

if (datestamp != currenttime) {
Console.WriteLine("");
Console.WriteLine("Readings from " + datestamp);
Console.WriteLine(" PID\tName"); currenttime = datestamp; } 
    Вот и все – проект завершен! Я знаю, он не так полезен, как остальные проекты, но у нас связаны руки, так как в Mono отсутствуют некоторые интересные возможности официальной реализации Microsoft. Не горюйте: работа над добавлением недостающих возможностей в Mono ведется постоянно. А пока вы можете много чего еще добавить в эту программу: например, возможность старта и остановки программы, определения периодов наибольшей загрузки системы и так далее. Творите! LXF
Персональные инструменты
купить
подписаться
Яндекс.Метрика