пятница, 24 июня 2011 г.

Зачем нам админы? Настраиваем Apache на VDS за 10 минут

Иметь "свой" VDS очень удобно. А порой и весьма полезно для повышения квалификации в смежных областях. Эта небольшая история началась с одной неудачной попытки поставить софт на VDS. Нужно было обновить пару библиотек, те потянули зависимости, а там уже нарисовалась перспектива замены ядра...
Когда стало ясно, что с моим Debian 5 проблем будет ещё много, я вспомнил, что мой VDS-провайдер предлагает ещё несколько образов диска на выбор, в том числе Ubuntu 10.4, версии библиотек у которой должны быть существенно новее. Но была одна проблема: в этом образе не предустановлена панель ISPManager. Эта панель замечательно помогала мне "рулить" моим сервером, не заглядывая в консоль. Было немного страшно отказываться от неё, ведь моя квалификация как сисадмина весьма невелика. Но что нам стоит попробовать? И, забэкапившись, начали...
Заливаем новый образ, входим root-ом, засекаем время :)
apt-get install apache2
Когда apache установлен, пробуем его запустить
service apache2 start
Если сервер поднялся без ошибок - значит вы не пожалели денег на конфигурацию виртуального сервера. Я пожалел... На 64 мб памяти apache смог только укоризненно написать в лог
Resource temporarily unavailable: apr_thread_create: unable to create worker thread
... и умереть.
Лог, кстати, по умолчанию лежит в /var/log/apache2/error.log
Прожорливость apache лечим редактированием лимитов в /etc/apache2/apache2.conf
Речь идёт о константах в блоках <IfModule mpm_prefork_module> <IfModule mpm_worker_module>, и <IfModule mpm_event_module>. Не долго думая ставим их вдвое меньше значений по умолчанию. Apache при старте может предложить поправить какие-либо константы. Послушаемся его (в меньшую сторону).
Итак apache запущен. При заходе на любой из своих сайтов, в DNS которых прописан ip моего сервера я вижу страницу apache по умолчанию с оптимистичной надписью.
Теперь настало время сконфигурировать виртуальные хосты для моих сайтов.
В большинстве мануалов это предлагается делать редактированием httpd.conf. Не верьте. В нашем случае httpd.conf пустой и оставлен только для совместимости. Вместо него стоит обратить внимание на два каталога /etc/apache2/sites-available и /etc/apache2/sites-enabled. Все файлы из последнего включаются в конфиг apache. И это не файлы на самом деле. Это ссылки на аналогичные файлы из первого каталога. Такая схема позволяет нам 1: не править основной конфиг при добавлении/изменении виртуальных хостов; 2: не править вообще ничего чтобы удалить/восстановить хост. Просто добавляем/удаляем соответствующую ссылку командой ln.
Кстати, та же схема работает и с включением/отключением модулей apache.
В /etc/apache2/sites-available есть конфиг виртуального хоста по умолчанию. Копируем его с новым именем (например именем домена) и правим, чтобы получилось что-то вроде этого:
<VirtualHost *:80>
    ServerAdmin webmaster@my1domain.com
    ServerName my1domain.com
    DocumentRoot /var/www/my1domain.com
    <Directory /var/www/my1domain.com>
        Options Indexes FollowSymLinks MultiViews
        AllowOverride All
        Order allow,deny
        allow from all
    </Directory>
    ErrorLog /var/log/apache2/my1domain-com-error.log
    LogLevel warn
    CustomLog /var/log/apache2/my1domain-com-access.log combined
</VirtualHost>
Тут my1domain.com - имя нашего домена. Для второго и т.д. сайта повторяем процедуру, соответственно меняя домен. Создаём ссылки на полученные конфиги в каталог /etc/apache2/sites-enabled и соответствующие каталоги для DocumentRoot. В эти каталоги и загружаем содержимое сайтов.
Делаем
service apache2 restart
и получаем ошибку (например) из-за того, что в .htaccess наших сайтов используется mod_rewrite а соответствующий модуль не подключен. Для подключения модуля, как вы уже догадались, делаем ссылку в /etc/apache2/mods-enabled которая указывает на /etc/apache2/mods-available/rewrite.load
В общем, с установленными но не подключенными модулями поступаем так всегда. А если модуль не установлен? Установим его. Например php5 ставим командой
apt-get install libapache2-mod-php5
Это только модуль для apache, сам php подтянется по зависимостям. 
Снова перезагружаем сервер и наслаждаемся работой своих сайтов. Впрочем, мы ведь забыли mysql? Не проблема:
apt-get install mysql-server
Вообще, идея этого поста не в том, как уложиться в 10 минут. Она в том, что не нужно бояться заниматься администрированием  своих серверов самостоятельно. Не нужно доставать саппорт провайдера или платить больше за навороченные панели управления, если нам всего лишь нужно захостить 2-3 ненагруженных сайта для своих экспериментов. А вот если эксперимент удастся, и нагрузка начнёт расти, тогда другое дело... Тогда наши суперуспешные проекты окупят нам и сервера и команду администраторов к ним. И решения этих сисадминов будут куда серьёзнее тех, что изложены выше. Но за это уже заплатите не вы, а ваши многочисленные и счастливые клиенты :)

четверг, 16 июня 2011 г.

svn over ssh: безопасный репозиторий для своих проектов в облаке

Редко кто из программистов не работает дома. И мало у кого не возникало проблемы синхронизации исходников между домашней и рабочей машиной. Есть масса решений от самых простых и неудобных (носим всё на флешке), до сложных или параноидальных (шифрованный контейнер в DropBox-е). Хочу описать ещё один вариант, который лично мне пока нравится больше всего.

Во-первых, однозначно нужна система контроля версий. Те кто хоть раз терял исходники со мной согласятся. А если над проектом работают и другие люди, сомнения в необходимости использования svn или т.п. вообще отпадают. Я лучше знаком с subversion, поэтому его и поставим. 

Во-вторых: куда ставить? Нужен как минимум VDS чтобы иметь root-доступ на удалённую машину для установки нужных нам приложений. Я выбрал firstvds.ru. В основном потому что минимальная конфигурация сервера (более чем достаточная для наших целей и парочки "домашних страничек" в придачу) там стоит около $5 в месяц. Можно поставить образ debian, senos, fedora, ubuntu c вполне достаточным набором софта или без него, по желанию. Я выбрал debian, поэтому остальные инструкции считаем применительно к нему. 

А инструкций, собственно, совсем немного:
1. Ставим svn
apt-get install subversion
2. Добавляем группу и пользователя
groupadd svn
3. Добавляем пользователя 
useradd -g svn -d /home/svn -m svn
4. Из-под нового пользователя создаём репозиторий
su svn
cd ~
mkdir repos
svnadmin create /home/svn/repos/
5. Чтобы закрыть доступ для всех кроме авторизованного пользователя и задать ему (т.е. себе) пароль, в файле /home/svn/repos/conf/svnserve.conf добавляем строки:
anon-access = none
auth-access = write
password-db = passwd 
и в файле passwd в том же каталоге добавляем строку вида
username = password
6. Запускаем сервер
/usr/bin/svnserve -d --listen-port 3232 -r /home/svn/repos/

И последнее: как получить доступ к нашей новой игрушке. Как указано выше, svnserve слушает у нас порт 3232. Поэтому самый простой способ получить доступ к репозиторию: svn://<хост или ip вашего сервера>:3232 Отсюда делаем checkout в локальный каталог на домашней машине (он, естественно, пока будет пустым), складываем в локальный каталог наши проекты и делаем commit (рекурсивно). Потом делаем checkout на рабочей машине с этого же адреса и получаем всё что положили. Дальше работаем как обычно. 
Но так работать не стоит, если не хотите, чтобы ваши исходники получил кто-то "третий". Протокол svn не защищен шифрованием. Есть несколько методов решения этой проблемы, но все они связаны с использованием дополнительного софта. Мы же поступим проще.
Порт 3232 на удалённой машине свяжем с портом (например) 2323 на локальной машине с помощью ssh-туннеля:
ssh -2 -N -f -L 2323:localhost:3232 root@<хост или ip вашего сервера>
Теперь url репозитория с которым можно безопасно работать:
svn://localhost:3232
Скорость работы в таком случае будет ниже, но можно не опасаться перехвата трафика.

среда, 8 июня 2011 г.

Пишем web-приложение для Google App Engine в NetBeans

Google App Engine даёт возможность бесплатного развёртывания небольших Java web приложений в облачной инфраструктуре google. Java-разработчик, который пишет под web, по-моему, просто обязан воспользоваться таким подарком. Хотя бы для эксперимента...
Как водится у google, есть SDK, масса хорошей документации и, конечно же, хороший плагин для Eclipse. Но мы пойдём другим путём :)
Попробуем "подружить" мою любимую IDE NetBeans с Google App Engine, собрать скелет проекта и загрузить его в облако.

Что нам нужно для этого?
Во-первых, само собой, NetBeans. Во-вторых SDK. После скачивания распаковываем его в любое удобное место. В-третьих плагин nbappengine. Устанавливаем его (в NetBeans 7.0) так:
1. Сервис -> Подключаемые модули -> Настройки -> Добавить
2. Указываем название источника (например Google App Engine) и адрес: http://kenai.com/projects/nbappengine/downloads/download/NetBeans69/updates.xml
3. Сохраняем новый источник и в закладке "Доступные подключаемые модули" ищем нужные нам пять модулей:
4. Устанавливаем их.
5. Сервис -> Серверы -> Добавить сервер. Выбираем в качестве сервера Google App Engine и указываем расположение каталога с распакованным SDK
На этом настройка инструментов завершена.

Регистрируемся в Google App Engine и регистрируем своё новое приложение
Надеюсь у вас уже есть google аккаунт. В этом случае вам понадобится пройти простую "дорегистрацию" на appengine.google.com, в процессе которой будет верифицирован с помощью sms ваш номер телефона. После регистрации можно создать своё приложение. Для этого нужно будет указать только "Application Identifier" (то, что будет в адресе приложения перед .appspot.com) и наименование приложения. У каждого разработчика есть лимит на 10 приложений. Поэтому советую на создавать десяток "хэловордов" а то для настоящего проекта квоты может и не хватить. Удалять приложения можно, но процедура эта небыстрая. После того как вы определились с идентификатором приложения и создали его, возвращаемся к NetBeans.

Создание приложения и загрузка его в Google App Engine
Создаём новый проект типа Java web application и на этапе выбора сервера указываем сервер Google App Engine.
В приложении, которое будет создано мастером, находим appengine-web.xml. В этом файле есть тег application. Вписываем туда Application Identifier, который зарегистрировали ранее. Собственно, на этом всё... Проект можно разрабатывать и отлаживать локально. А по достижении какой-либо стадии готовности, залить в облако. Для этого кликаем правой клавишей на проекте и выбираем опцию "Deploy to Google App Engine". В процессе заливки нужно будет ввести свой google логин и пароль. Сразу после загрузки приложение доступно вашим клиентам. 
Управлять приложением, например активировать ту или иную его версию, следить за расходом ресурсов и т.п. можно из панели администрирования на appengine.google.com. Но это уже тема для следующего рассказа. 

понедельник, 6 июня 2011 г.

Android: список с элементами произвольного вида

Что такое "классический интерфейс Android"? Однажды мы с нашим дизайнером искали ответ на этот вопрос. Ответ нашёлся просто. Открываем самое что ни на есть стандартное приложение - "Настройки" и что мы там наблюдаем?



Списки. ListView. Только выглядят они очень по-разному. И если мы решили сделать в своём приложении всё "классически", то нужно уметь поставить элементом списка любой набор View, который требуется в данном контексте логикой нашего приложения.
Делаем это в три этапа.

1. XML-layout элемента
В каталоге res/layout нашего проекта создаём xml-файл с разметкой элемента списка.
Например такая разметка:
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:gravity="center_vertical"
  android:orientation="horizontal">
  <ImageView
    android:id="@+id/contact_item_icon"
    android:layout_width="50sp"
    android:layout_height="50sp"
    android:padding="5sp"/>
  <LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:orientation="vertical">
    <TextView
      android:id="@+id/contact_item_title"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="16sp"
      android:textStyle="bold"/>
    <TextView
      android:id="@+id/contact_item_details"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="10sp"/>
  </LinearLayout>
</LinearLayout>
выведет такой список:







2. Создание ListAdapter-a
Это, пожалуй, наиболее неочевидная часть процесса. ListView для построения списка нуждается не только в данных и разметке. Для корректного наложения первого на второе нужен класс, расширяющий ArrayAdapter. Вот как он выглядит в нашем случае:
public class ContactListAdapter extends ArrayAdapter {

  private Contact[] conts;
  private Context context;

  public ContactListAdapter(Context context, int textViewResourceId, Object[] conts) {
    super(context, textViewResourceId, conts);
    this.conts = (Contact[]) conts;
    this.context = context;
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    View v = convertView;
    if (v == null) {
      LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      v = inflater.inflate(R.layout.image_list_item, null);
    }
    TextView bTitle = (TextView) v.findViewById(R.id.contact_item_title);
    TextView bDetails = (TextView) v.findViewById(R.id.contact_item_details);
    ImageView iView = (ImageView) v.findViewById(R.id.contact_item_icon);
    Contact c = conts[position];
    bTitle.setText(c.getDisplayName());
    if (c.getIcon()!=null) {
      iView.setImageBitmap(c.getIcon());
    } else {
      iView.setImageResource(android.R.drawable.ic_menu_gallery);
    }
    if(c.getPhone().size()>0) {
      bDetails.setText(c.getPhone().get(0).getNumber());
    } else if(c.getEmail().size()>0) {
      bDetails.setText(c.getEmail().get(0).getAddress());
    } else if(c.getImAddresses().size()>0) {
      bDetails.setText(c.getImAddresses().get(0).getName());
    }
    return v;
  }
}

Конструктор принимает ссылку на ресурс xml, в котором описана разметка элемента списка а также массив объектов, которые должны быть представлены в виде списка. В данном случае - это контакты из списка нашего телефона. Переопределённый метод getView связывает поля нашего класса с объектами из разметки.

3. Ну, и, собственно, вызов...
ListView lw = (ListView) findViewById(R.id.contacts_list);
lw.setAdapter(new ContactListAdapter(this, R.layout.image_list_item, items));

Тут items - наш массив контактов.

пятница, 3 июня 2011 г.

Работа со звуком в Android-приложении

На днях возникла задача: получить и проанализировать амплитуду сигнала с микрофона в Android. Задача, в принципе, простая. В обычном случае мы используем класс MediaRecorder таким вот образом:
MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
recorder.setOutputFile("/dev/null");
recorder.prepare();
recorder.start();
for(int i=0; i<10; i++) {
 System.err.println("current amplitude is:"+recorder.getMaxAmplitude());
 try {
   Thread.sleep(1000);
 } catch (Exception e) {}
}
recorder.stop();
recorder.reset();
recorder.release();

В этом фрагменте мы теоретически должны получить 10 отметок максимальной амплитуды сигнала за периоды чтения по секунде с первой до десятой. Для этого мы читаем сигнал с микрофона в "никуда", и используем getMaxAmplitude(), который согласно документации должен возвращать значение максимальной амплитуды за период с последнего вызова метода до данного момента. В чём проблема? На моём LG P500 этот метод всегда возвращает ноль :(
Исследования на тему "Почему" дали немного. Гораздо интереснее ответ на вопрос

Как всё-таки получать амплитуду сигнала?
Класс MediaRecorder крайне неудобен для работы с сигналом напрямую. Даже если бы я смог его использовать, запись данных в файл - далеко не всё, что обычно нужно при захвате звука. Для наложения фильтров и анализа сигнала удобнее получить сигнал в массив и работать с ним "на лету". К счастью есть ещё один класс в Android SDK, который нас выручит. Это AudioRecord.
Работать с ним несколько сложнее, но мы не боимся трудностей :)

public class AmplitudeReader extends Thread {
 
  private AudioRecord audioRecord;
  private int bufflen;
  private static final int SAMPPERSEC = 48000;
 
  public AmplitudeReader() {
    android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
    int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_DEFAULT;
    int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
    bufflen = AudioRecord.getMinBufferSize(SAMPPERSEC, channelConfiguration, audioEncoding);
    audioRecord = new AudioRecord(android.media.MediaRecorder.AudioSource.MIC, SAMPPERSEC, channelConfiguration, audioEncoding, bufflen);
    audioRecord.startRecording();
  }
 
  private short getMax(short[] arr, int count) {
    short m = 0;
    for (int i = 0; i < count; i++) {
      short c = (short) Math.abs(arr[i]);
      if (m < c) {
        m = c;
      }
    }
    return m;
  }
 
  @Override
  public void run() {
    for(int i=0; i<11; i++) {
      short[] curr = new short[bufflen];
      int currread = audioRecord.read(curr, 0, bufflen);
      System.err.println("current amplitude is:" + getMax(curr, currread));
    }
    audioRecord.stop();
    audioRecord.release();
  }
}

В этом примере мы выводим в лог десять значений амплитуды, снятые "на лету" с каждого буфера чтения. Размер буфера система нам установит сама исходя из настроек. Обычно это 4Кb. Понятно, что получением амплитуды мы можем не ограничиться. Сигнал после считывания представлен у нас в массиве байтов (или short, смотря какой метод использовать для чтения). Совершая с элементами этого массива различные действия мы можем усиливать сигнал, подавлять постоянную составляющую, сглаживать и т.п.
И ещё одна особенность: для использования MediaRecorder нам понадобятся 2 разрешения: на чтение и на запись звука на карту: android.permission.RECORD_AUDIO и android.permission.WRITE_EXTERNAL_STORAGE, для использования AudioRecord - только первое из них.