пятница, 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 - только первое из них.

вторник, 24 мая 2011 г.

Android: передача объектов между Activity

В одном из предыдущих постов мы затронули тему перехода между Activity в Android-приложениях. И, в частности, обсуждали передачу данных из одного Activity в другое. Очевидное решение  intent.putExtra(NextActivity.FIELD_NAME, field_value) хорошо служит нам до тех пор, пока мы передаём данные "простых" типов: String, int, long или массивы. Но стоит нам попытаться передать таким образом объект из нашей модели данных, начинаются трудности.
Объекты таким способом передавать можно, но они должны реализовывать интерфейс Serializable или Parcelable. Первый вариант по некоторым найденным мной отзывам, снижает производительность приложения. Второй - давайте рассмотрим внимательнее.
Вот пример класса одного из моих приложений:

  1. public class Email implements Parcelable {
  2.  
  3.   private String address;
  4.   private String type;
  5.  
  6.   private Email(Parcel in) {
  7.     this.address = in.readString();
  8.     this.type = in.readString();
  9.   }
  10.  
  11.   public String getAddress() {
  12.     return address;
  13.   }
  14.  
  15.   public void setAddress(String address) {
  16.     this.address = address;
  17.   }
  18.  
  19.   public String getType() {
  20.     return type;
  21.   }
  22.  
  23.   public void setType(String t) {
  24.     this.type = t;
  25.   }
  26.  
  27.   public Email(String a, String t) {
  28.     this.address = a;
  29.     this.type = t;
  30.   }
  31.  
  32.   public int describeContents() {
  33.     return 0;
  34.   }
  35.   public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
  36.  
  37.     public Email createFromParcel(Parcel in) {
  38.       return new Email(in);
  39.     }
  40.  
  41.     public Email[] newArray(int size) {
  42.       return new Email[size];
  43.     }
  44.   };
  45.  
  46.   public void writeToParcel(Parcel parcel, int i) {
  47.     parcel.writeString(address);
  48.     parcel.writeString(type);
  49.   }
  50. }
Как видим,он реализует интерфейс Parcelable, а значит может легко передаваться между Activity. Как мы этого добиваемся? Мы дополняем логику класса методами для его Parcel-изации и Де-parcel-изации, проще говоря, сохранения в Parcel и восстановления из него. Parcel тут можно рассматривать как некий буфер, в который можно сложить в определённом порядке данные любых типов и затем (в том же порядке !) их оттуда извлечь.
Складывать содержимое полей класса в Parcel очень просто. Это реализуется в методе writeToParcel. Для восстановления объекта действуем так: Создаём вложенный класс Parcelable.Creator, в котором реализуем два метода. Второй выглядит всегда одинаково, а вот первый для нас весьма важен: он вызовет новый конструктор нашего класса, передавая в него Parcel. В конструкторе мы должны реализовать логику, обратную методу writeToParcel, т.е. вычитать из Parcel-а значения полей класса в том же порядке, в каком их туда записывали. В случае простых полей типа String это несложно. Если же у нас есть поля типа ArrayList, то делаем так:
p.writeInt(phone.size());
for (int i = 0; i < phone.size(); i++) {
  p.writeString(phone.get(i));
}
для сохранения, и
int count = p.readInt();
for (int i = 0; i < count; i++) {
  String ph = p.readString();
  phone.add(ph);
}
для восстановления. Тут p, как вы, наверное, догадались - объект класса Parcel, а phone - поле типа ArrayList.
Если полями нашего класса являются другие наши классы, то мы должны их также "научить Parcel-изоваться".  Тогда поля таких типов мы будем укладывать в Parcel методом
p.writeParcelable(organization, Parcelable.CONTENTS_FILE_DESCRIPTOR);
и извлекать оттуда методом
organization = p.readParcelable(getClass().getClassLoader());