воскресенье, 19 декабря 2010 г.

Web: Tree-graph visualization in javascript

Возникло желание нарисовать на web диаграмму в виде дерева, в стиле "Исаак родил Иакова; Иаков родил Иуду". И при этом позволить перетаскивать элементы дерева, и сохранять расположение нод.

Оказалось что задача вовсе не тривальна и состоит как минимум из трех частей:
  • Раскладка графа в виде дерева
  • Рисование соединительных линии между блоками
  • Возможность сохранения и восстановления результата.


Я попытался найти готовую JavaScript библиотеку для этого. Вот что из этого вышло:

Dracula Graph Library

Рисует используя SVG и обладает очень простым синтаксисом. К сожалению библиотека представляет лишь один метод визуализации графа:



При этом синтаксис очень прост:
window.onload = function() {
    var g = new Graph();
    g.addNode("Root");
    g.addNode("Vasy");
    g.addNode("Pety");
    g.addNode("Sasha");
    g.addNode("Masha");
    g.addNode("Katy");
    g.addNode("Misha");
    g.addEdge("Root", "Vasy");
    g.addEdge("Root", "Pety");
    g.addEdge("Vasy", "Sasha");
    g.addEdge("Vasy", "Masha");
    g.addEdge("Pety", "Katy");
    g.addEdge("Pety", "Misha");
    var layouter = new Graph.Layout.Spring(g);
    layouter.layout();
    var renderer = new Graph.Renderer.Raphael('canvas', g, width, height);
    renderer.draw();
 


Если бы была возможность раскладывать граф в дерево и сохранять в JSON расположение нод, то это была бы идеальная библиотека для моих целей.


js-graph-it



Тут сами ноды и соединения задается в виде набора html тэгов с предопределенными классами.

<p class="block draggable" style="top: 100px; left: 10px; width: 300px;" id="par_1">
Ioan
</p>
<p class="block draggable" style="top: 200px; left: 100px; width: 300px;" id="par_2">
Yakov
</p>
<p class="block draggable" style="top: 300px; left: 100px; width: 300px;" id="par_3">
Iuda
</p>
<p class="block draggable" style="top: 400px; left: 100px; width: 300px;" id="par_4">
Vasy
</p>
<div class="connector par_1 par_2">
    <img class="connector-end" src="arrow.gif">
</div>
<div class="connector par_2 par_3">
    <img class="connector-end" src="arrow.gif">
</div>
<div class="connector par_2 par_4">
    <img class="connector-end" src="arrow.gif">
</div>
 

К сожалению библиотека требует, чтобы в разметке уже была рассчитана начальная раскаладка графа. Это удобно лишь для отрисовки статических графов, но для рисования динамических нужно самому реализовывать расчет местоположения всех нод.
К плюсам можно отнести то, что все соединения рисуются стандартными div'ами т.е. работать будет почти везде, но при этом все связи будут состоять из вертикальных или горизонтальных линии, кривых и диагональных нет.

Пока найти 100% подходящую под мои требования библиотеку, мне не удалось. :(
Вот обсуждение на StackOferflow этих и других библиотек.

среда, 15 декабря 2010 г.

SharePoint: PowerShell script for creating big list with look up fields

В комментариях к моему предыдущему посту где я писал утилитку для создания больших листов c лукап полями на C#, высказали мнение о том, что неплохо бы такие вещи писать на PowerShell. Ну я и попробовал, вот версия на PowerShell:

PARAM ($url, $toCount, $toListItemsCount, $lookUpItemsCount)

$snapin = Get-PSSnapin | Where-Object {$_.Name -eq 'Microsoft.SharePoint.Powershell'}
if ($snapin -eq $null){
    Add-PSSnapin Microsoft.SharePoint.Powershell
    [Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
}

$spWeb = Get-SPWeb -Identity $url
$toListsIds = @()

for($iList=0; $iList -lt $toCount; $iList++)
{
    $toListName = "ToList" + $iList
    Write-Host "Creating to list " $toListName
   
    $lstId = $spWeb.Lists.Add($toListName, "test to list",100);
    $spLst = $spWeb.Lists.GetList($lstId, $true);  
    $toListsIds+=$lstId
   
    for ($iItem=0; $iItem -lt $toListItemsCount; $iItem++)
    {
        $spItem = $spLst.Items.Add()
        $spItem["Title"] = [string]::Format("List {0} item {1}", $toListName, $iItem)
        $spItem.Update()
        Write-Host "\tadd item " $iItem
    }
}

Write-Host "Creating look up list"
   
$lookUplstId = $spWeb.Lists.Add("LookUpList", "test to list",100); 
$splookUpLst = $spWeb.Lists.GetList($lookUplstId, $true);  

for ($iField=0; $iField -lt $toCount; $iField++)
{
    $fldIntName = $splookUpLst.Fields.AddLookup("LookUpField"+$iField,  $toListsIds[$iField], $false)
    $spFld = $splookUpLst.Fields[$fldIntName]
    $spLookUpFld = $spFld -as [Microsoft.SharePoint.SPFieldLookup]
    $spLookUpFld.ShowInDisplayForm = $true
    $spLookUpFld.LookupField = "Title"
    $spLookUpFld.Update()
    Write-Host "\tadd field " $iField
}

Write-Host "Creating look up items " $lookUpItemsCount
   
$rand = New-Object system.random

for ($iItem=0; $iItem -lt $lookUpItemsCount; $iItem++)
{
    $spItem = $splookUpLst.Items.Add()
    $spItem["Title"] = "Item " +  $iItem

    for ($iField=0; $iField -lt $toCount; $iField++)
    {
        $fldName = "LookUpField"+$iField
       
        if ($iField -eq 0)
        {
             $spItem[$fldName] = $null
        }
        else
        {
            $rId = $rand.Next(0, $toListItemsCount)
             
            if ($rId -eq 0)
            {
                $spItem[$fldName] = $null;
            }
            else
            {
                $spItem[$fldName] = $rId;
            }
        }
    }
    $spItem.Update()   
    Write-Host "Create item " $iItem
}

Write-Host "DONE!"


В написании скрипта мне очень помог бесплатный редактор PowerGUI

вторник, 14 декабря 2010 г.

SharePoint: Creating big list with look up fields

Понадобилось мне тут проверить, как будет вести себя мой продукт с большими листами содержащими LookUp поля. Создавать руками это не наш метод, потому набросал код который в определенном сайте создает листы и лист с лукапами на них.

Вот такой запуск:
LookUpCreator http://server/site1 10 10 100

создаст 10 листов "ToList" с 10 итемами вида "Item " и лист "LookUpList" содержащий 10 лукапных колонок на вышеозначенные листы и содержащий 100 итемов.

Хочу отметить что первая лукапная колонка не содержит значении специально. И так же часть значении не заполнены. Этим я имитировал некоторые нужные мне ситуации.

        static void Main(string[] args)
        {
            Console.WriteLine("LookUps creator usage <site url> <to lists count> <to lists items> <lookup items count>");

            var siteUrl = args[0];

            var toCount          = int.Parse(args[1]); //   10;
            var toListItemsCount = int.Parse(args[2]); //   10;
            var lookUpItemsCount = int.Parse(args[3]); // 7000;

            var listToBaseName = "ToList";
            var lookUplistName = "LookUpList";

            using (var site = new SPSite(siteUrl))
            {
                using (var web = site.OpenWeb())
                {
                    Console.WriteLine("Creating to {0} lists with {1} items", toCount, toListItemsCount);
                    var toLists = new List<SPList>(toCount);
                    for (int iList=0; iList<toCount; iList++)
                    {
                        var toListName = listToBaseName + iList.ToString();
                        Console.WriteLine("Creating to list '{0}'", toListName);

                        var lstId = web.Lists.Add(toListName, "test to list",SPListTemplateType.GenericList);
                        var spList = web.Lists.GetList(lstId, true);

                        toLists.Add(spList);
                       
                        for (int iItem = 0; iItem < toListItemsCount; iItem++)
                        {
                            var item = spList.Items.Add();
                            item["Title"] = string.Format("List '{0}' item {1}",toListName,iItem );
                            item.Update();
                            Console.WriteLine("\tadd item {0}", iItem);
                        }
                    }

                    Console.WriteLine("Creating look up list {0}", lookUplistName);

                    var lookUplstId = web.Lists.Add(lookUplistName, "test lookup list", SPListTemplateType.GenericList);
                    var spLookUpList = web.Lists.GetList(lookUplstId, true);

                    for (int iField = 0; iField<toCount; iField++)
                    {
                        var fldName = "LookUpField"+iField.ToString();
                        var fldIntName = spLookUpList.Fields.AddLookup(fldName, toLists[iField].ID, false);
                        var fldLookUp = spLookUpList.Fields[fldIntName] as SPFieldLookup;                        
                        fldLookUp.ShowInDisplayForm = true;
                        fldLookUp.LookupField = "Title";
                        fldLookUp.Update();
                        Console.WriteLine("\tadd column '{0}'", fldIntName);
                    }

                    Console.WriteLine("Creating {0} look up items", lookUpItemsCount);

                    var Rnd = new Random();
                    for (int iItem = 0; iItem < lookUpItemsCount; iItem++)
                    {
                        var spItem = spLookUpList.Items.Add();

                        spItem["Title"] = "Item " + iItem.ToString();

                        for (int iField = 0; iField < toCount; iField++)
                        {
                            var fldName = "LookUpField" + iField.ToString();

                            if (iField == 0)
                                spItem[fldName] = null;
                            else
                            {
                                var rId = Rnd.Next(0, toListItemsCount);
                                if (rId == 0)
                                    spItem[fldName] = null;
                                else
                                    spItem[fldName] = rId;
                            }
                        }
                        spItem.Update();
                        Console.WriteLine("\t" + spItem.Title);
                    }

                    Console.WriteLine("Done!");
                }
            }
        }
 


Может быть, кому-нибудь пригодится или послужит примером. UPD: Вариант этого же кода, но на PowerShell тут

четверг, 9 декабря 2010 г.

SharePoint: How to delete folder with items using web services.

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

Сегодня надо было удалить folder с файлами внутри Document Library используя Web Services. Для этого есть Lists.UpdateListItems

Вот что у меня вышло:
var url = "http://server/subsite/_vti_bin/Lists.asmx";

var xml = "<Batch OnError=\"Continue\" PreCalc=\"TRUE\" ListVersion=\"0\">"+
                "<Method ID=\"1\" Cmd=\"Delete\">"+
                "<Field Name=\"ID\">10</Field>"+
                "<Field Name=\"FileRef\">http://server/subsite/DocLib/Folder</Field>" +
                "</Method>"+
                "</Batch>";

var listWebSvc = new ListsSvc.Lists();
listWebSvc.Url = url;
listWebSvc.Credentials = System.Net.CredentialCache.DefaultCredentials;
listWebSvc.PreAuthenticate = true;

var xmlDoc = new System.Xml.XmlDocument();
xmlDoc.LoadXml(xml);

var res = listWebSvc.UpdateListItems("DocLib",xmlDoc);
 

Хочу обратить внимание на то что в FileRef нельзя указывать путь относительно сайта ("/DocLib/Folder"), я на это сразу не обратил внимание, и потратил лишнее время в поисках почему оно не работает.

воскресенье, 28 ноября 2010 г.

Книга: "Deadline Роман об управлении проектами"

Закончил читать очередную книгу Тома Демарко "Deadline Роман об управлении проектами".
Книжка мне очень понравилась, прежде всего нестандартным способом изложения материала. Это именно роман, с сюжетом, героями и даже небольшой любовной линией.
По сюжету герой, мистер Вебстер Томпкинс, управляет большим предприятием по выпуску софта и сталкивается с разными ситуациями. Выводы же сделанные в процессе работы он заносят в свою записную книжку.

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

Конечно же, это очередная книга-сказка об управлении сказочными людьми, в сказочном королевстве, но все же она позволяет задуматься, провести параллели с реальными ситуациями и возможно даже натолкнет на решения.

Считаю что книга достойна занять место на моей полке с литературой.

пятница, 12 ноября 2010 г.

How to install Etherpad on Windows

Послушав, как здорово рассказывают ребята из Yandex про командную работу, и использование, для совместного планирования, Etherpad'а я решил его поставить на одной из своих тестовых машин.
Linux систем у меня в хозяйстве нет, потому ставить решил на виртуальную машину с Windows 2008 R2 (x64), на которой крутится еще куча всего.

Для начала нашел вот эту инструкцию и попытался выполнить ее. Но cтолкнулся с рядом трудностей, посему попытаюсь ее перевести и немного дополнить.

Если кратко то надо сделать следующее:
Поставить:
  • MySQL
  • mysql-connector-java-5.1
  • CygWin т.к. все файлы сборки заточены под posix команды
  • Java JDK (я ставил jdk1.6.0_22)
  • Scala, причем ставить надо не последнюю (на момент написания 2.8.0), а предыдущую - 2.7.7. В противном случае на этапе компиляции получите ошибку "error not found type byte"
  • Сам Etherpad, причем редакцию предназначенную именно для windows, вот отсюда.

После чего надо создать фолдер, не содержащий пробелы, например "C:\Prog". И сделать в него символические линки используя либо вот эту програмку, либо иной способ например Alt+F6 в FAR Manager.

В результате должна получится следующая структура каталогов:

  • С:\Prog\Java\jdk1.6.0_22
  • С:\Prog\MySQL\MySQL Server 5.1
  • С:\Prog\Scala
  • С:\Prog\mysql-connector-java-5.1.13-bin.jar

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

  • JAVA_HOME = С:\Prog\Java\jdk1.6.0_22
  • SCALA_HOME = С:\Prog\Scala
  • JAVA = С:\Prog\Java\jdk1.6.0_22\bin\java
  • SCALA = С:\Prog\Scala\bin\scala
  • MYSQL_CONNECTOR_JAR = С:\Prog\mysql-connector-java-5.1.13-bin.jar

А так же добавить вот эти пути к переменной среды PATH:

  • C:\CygWin\bin
  • C:\Prog\MySQL\MySQL Server 5.1\bin
  • C:\Prog\Scala\bin
  • C:\Prog\Java\jdk1.6.0_22\bin

Причем, крайне важно добавить путь C:\CygWin\bin в начало PATH!. В противном случае на этапе компиляции получим ошибку связанную с find. Т.к. вместо find из CygWin будет использоваться системный find, а он использует совершенно иной синтаксис.

После всего этого создаем базу в mysql для EtherPad:
Запускаем интерпретатор команд:
mysql -u root -p

И выполняем вот такие команды :
create database etherpad;
grant all privileges on etherpad.* to 'etherpad'@'localhost' identified by 'password';
quit

Ну и теперь запускаем cmd.exe
Делаем cd C:\etherpad-win\trunk\etherpad
И запускаем компиляцию bash bin\rebuildjar.sh

Если все сделано верно то компиляция завершится без ошибок.

И можно будет запустить сервис командой run-local.bat

После чего зайдите на http://localhost:9000 и насладитесь работающим Etherpad'ом.

Но если попытаться зайти на него снаружи по адресу http://myserver:9000 то получите сообщение в браузере invalid superdomain.

Для того, чтобы работать удаленно надо открыть файл C:\etherpad-win\trunk\etherpad\src\etherpad\globals.js и добавить в список доменов наш, например вот так:
var SUPERDOMAINS = {
'localbox.info': true,
'localhost': true,
'etherpad.com': true,
'myserver': true
};

После чего снова все пересобрать и на этот раз все должно заработать.

Надеюсь установка и настройка этого тула сможет способствовать улучшению коммуникаций в вашей команде.

суббота, 6 ноября 2010 г.

Книга: "Человеческий фактор"

С удовольствием прочел книгу Тома Демарко и Тимоти Листера "Человеческий фактор". Написано весело и легко. В ненавязчивой манере позволяет улучшить понимание того как формируются команды и как этим командам помочь достичь успеха. Вот несколько тезисов которые мне особо понравились в этой книги:
  • Серьезные проблемы имеют не сколько технологическую, сколько социологическую природу.
  • Многие руководители слишком много времени уделяют технологиям, забывая что человеческий фактор
  • Команды это наши рабочие единицы, а потому наша область деятельности - преимущественно человеческое взаимодействие.
  • Поощрение атмосферы, не позволяющей допускать ошибки, заставляет людей занимать оборонительные позиции.
  • Именно уникальность участников команды является залогом активности и эффективности.
  • Единственное стабильное состояние в жизни проекта - трупное окоченение
  • Каждый час сверхурочных компенсируется часом недоработки
  • Трудоголизм - это заболевание
  • Команда имеет право вето на сдачу продукта если по ее мнению он не готов,и не имеет значение готов ли клиент принять такой продукт
  • Даже в тех редких случаях, когда давление на человека остается единственным вариантом, руководитель должен оказывать это давление последним
  • Проекты, в которых шеф не оказывал давления, характеризовались самой высокой производительностью
  • Единственным доказательством того, что открытая планировка улучшает производительность, всегда служит упорное повторение этого утверждения
  • Поток - это состояние глубокого, почти медитативного погружения в работу
  • Сотрудник, который постоянно пытается войти в поток и постоянно вынужден отвлекаться, - человек недовольный
  • В большинстве офисов, шума и помех достаточно, чтобы затруднить серьезные мыслительные процессы до невозможности
  • Работников не очень заботит вид офиса, но их заботит шум, уединенность и площадь стола
  • Большая часть ошибок при найме вызвана излишним вниманием к внешности кандидата и недостаточным к их способностям
  • Нанимая человека попросите его подготовить краткую презентацию по известной ему теме для всей команды, а потом обсудите его со всей командой
  • Вы причините меньший ущерб организации, если выстроите сотрудников перед дулом пулемета, чем если организуете переезд
  • Ничто не может лишить сотрудников мотивации настолько эффективно, насколько это сделает осознание ими того факта, что руководство считает их некомпетентными
  • Вера в то, что сотрудники автоматически принимают цели организации, признак наивного оптимизма руководства.
  • Лучший успех - тот, в котором нет очевидного участия руководства, а команда работает как содружество равных.
  • Человек которому нельзя доверить автономную работу, бесполезен
  • Руководитель находится за пределами команды и время от времени подчеркивает направление развития и убирает административные препятствия
  • Команда - это сеть, а не иерархия. Лидерству здесь нет места.
  • Женщины работают не чуть не хуже мужчин, и в команде они должны быть непременно.
  • В работе должна быть возможность для беспорядка ,это могут быть пилотные проекты, игры, мозговые штурмы, путешествия, конференции etc. Это объединяет команды и способствует их кристаллизации
  • Лучшие руководители способны выделять тех кто сочетает перспективу и зрелость и позволить им работать самим. Так они принесут больше пользы компании
  • Взаимное обучение сотрудников возможно лишь при отсутствии внутренней конкуренции
  • Люди не любят перемен т.к. не уверены что у них все получится. Для успешных изменении люди должны быть уверены что при любых результатах их не накажут.
  • Переменам способны помочь лишь те кто в них верит, но способен оспорить. Те кто слепо верит и те кто активно противодействуют - враги перемен.
  • Любые регулярные собрания это скорее ритуалы и они не несут пользы
Рекомендую прочесть целиком.

понедельник, 1 ноября 2010 г.

Log4net: Issue with RollingFileAppender

Достаточно давно работаю с Log4Net и в принципе нахожу его весьма неплохим и простым в использовании инструментом.
Причем пользуюсь я им именно в режиме ротации логов (RollingFileAppender)

Настраиваю его как то вот так:
    <appender name="all" type="log4net.Appender.RollingFileAppender">
      <file value=".\All.log" />
      <appendToFile value="true" />
      <maxSizeRollBackups value="1000" />
      <maximumFileSize value="10000" />
      <rollingStyle value="Size" />
      <staticLogFileName value="true" />
      <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date [%thread] %-5level %logger [%property{version}] - %message%newline" />
      </layout>
    </appender>
 

Недавно столкнулся с тем, что, иногда в папке с логами, вижу дикое количество очень коротких фрагментов лога. Работать с такими обрезками совершенно не возможно.

После некоторого исследования оказалось, что происходит это, если основной файл лога (в нашем случае All.log) был залочен (например открыт в редакторе), после этого механизм ротации логов перестает работать человеческим образом и начинает генерировать файлы вида All.log.123 при каждом создании логера этого типа.
Причем если убрать +MinimalLock то картина становится еще хуже т.к. новые файлы просто не создаются, а переименовывается один и тот же.

Без выключения staticLogFileName мне это победить не удалось, а с staticLogFileName мне не подходит.

понедельник, 18 октября 2010 г.

How to Backup VMWare ESXi 4 Server

Как известно люди деляться на две категории, тех то еще не делает бэкапы и тех кто их уже делает.

На днях на одном из моих серверов с виртуальными машинами накрылся HDD, а, поскольку, RAID1 (зераклирование) я отключил из соображении быстродействия, то остался я у разбитого корыта. Повезло лишь в том, что у меня виртуальные машины, достаточно, простые и поднятие новой упирается лишь во время.

Но в то же время очень бы не хотелось попасть в такую ситуацию еще раз.
Поиск в интернете на тему бэкапа ESXi, дал множество продуктов, которые это умеют делать, но за деньги. Увы это не наш путь.
Посему решил написать небольшой скрипт для бэкапа.

Бэкапить я собираюсь на Win машину.

Для того, чтобы к ESXi можно было подключаться удаленно, необходимо его перевести в unsupported mode и включить ssh. А так же установить пакет putty.

После чего делаем вот такой батничек с именем backuplab.bat:

@ECHO OFF
REM %1 - IP of vm host
REM %2 - root password
REM %3 - datastorename
REM %4 - lab name
REM %5 - backup path

ECHO Backup %4 from %1 host to %5\%4

ECHO Create folder for backup - %5\%4

md %5\%4

REM ECHO Remove old data - %5\%4
REM del /F /Q %5\%4\*.*

ECHO Get data from host

"%ProgramFiles%\PuTTY\pscp.exe" -l root -pw %2 -scp -unsafe %1:/vmfs/volumes/%3/%4/*.* %5\%4

ECHO Compress backup

REM C:\Program Files\WinRARrar.exe" a

"%ProgramFiles%\WinRar\rar.exe" a -r -ep1 %5\%4\labarh.rar %5\%4\*.*

ECHO Remove uncompressed data

del /F /Q %5\%4\*.vm*


Использовать его можно так :

backuplab.bat 10.30.30.111 mysuperpassword datastore1 LAB01 C:\BACKUPS


Параметры:
  • IP адрес или имя хоста где живут виртуалки
  • пароль рута
  • название datastore
  • имя виртуальной машины
  • каталог для бэкапов

Скрипт делает следующее -
  1. В каталоге C:\BACKUPS создает подкаталог с именем машины - LAB01
  2. Вытаскивает все данные из датасторы на ESXi
  3. архивирует полученное в labarh.rar

Работает он конечно не шустро, но меня пока устраивает.

PS: Для того чтобы понять что и как лежит в датасторах можно использовать WinSCP

UPD: Вышеописанный метод, с архивацией бэкапа WinRar дает очень неплохой результат, например для моей виртуальной машины объемом 21G бэкап размером всего в 3.5G.

вторник, 5 октября 2010 г.

Mercurial on Windows Server

Последнее время я увлекся системой децентрализованного контроля версии Mercurial. Нравится мне эта штука прежде всего своей легкостью и ненавязчивостью.
В начале я использовал его для индивидуальных проектов для возможности иметь историю изменении и возможность ветвления.
Для такого использования надо, всего лишь, поставить вот это

После чего в любом каталоге будет достаточно набрать:

hg init
hg add
hg commit -m "first"

и все файлы будут зачекинены в репозитории.

Если есть желание использовать репозитории удаленно, то это тоже достаточно не сложно.
Я буду рассматривать вариант поднятия сервера под Windows.

Предположим мы хотим сделать 2 репозитория в одном каталоге сервера:

D:\REPOS\
D:\REPOS\firstproject
D:\REPOS\secondproject


Для этого в каталог D:\REPOS\ кладем вот такой конфиг web_dir.config:

[paths]
firstproject = D:/REPOS/firstproject
secondproject = D:/REPOS/secondproject

[web]
allow_push = *
push_ssl = false
contact = Bilbo Baggins
description = fixes
allow_archive = zip


После этого в каталоге D:\REPOS\ выполняем командочку

hg serve --webdir-conf web_dir.config


Не очень очевидно то что после запуска вы ничего на консоли не увидите и управление вам не вернется. :)

Попробуите заити по адресу http://localhost:8080 ,должны увидеть простой веб интерфейс с двумя проектами.

Остальные параметры значат следующее
  • allow_push - * - разрешает пушить в репозитории всем
  • push_ssl = false - разрешает пушить по http (по умолчанию только ssl)
  • contact = Bilbo Baggins - имя того кто отвечает за проект (для каждого проекта можно переопределить в .hg\hgrc для конкретного репозитория )
  • description = fixes - описание проекта
  • allow_archive = zip - добавляет ссылочку для скачивания последней версии в архиве

Пользоваться этим просто. На клиентской машине так же ставим mercurial и даем команду:

hg clone http://myserver/firstproject


Теперь у нас есть своя копия репозитория локально и можно с ней работать, после того как сделали изменения и их зачекинили (hg commit) ,можно их вернуть обратно на сервер командой hg push или взять свежее с сервера командой hg pull

При этом в веб интерфеисе можно легко смотреть изменения , коменттарии и прочее.

Вообщем все очень просто и удобно.

Для более подробного понимания всех команд hg RTFM User Guide

вторник, 28 сентября 2010 г.

Fix vs Feature

Все рано или поздно сталкиваются с тем, что в продукте на стороне заказчика возникают ошибки. Бывают они разного уровня критичности и у разного количества заказчиков. Зачастую реакция на такие запросы требует оперативности и делается в достаточно нервных условиях.

Мне бы хотелось выразить свою точку зрения на то что есть 'фикс' и чем он отличается от полноценной 'фичи'.

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

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

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

В любом случае перед тем как делать что-нибудь стоит ответить на следующие вопросы:

  • Знаем ли мы как это быстро поправить?
  • Существует ли способ обойти ситуацию без изменении в коде? (workaround)?
  • Сильно ли эта ситуация мешает заказчику?
  • Много ли заказчиков столкнулось с этой ситуацией?
  • Уверены ли мы отсутствии побочных эффектов после правки?
  • Есть ли у нас всеобъемлющее решение чтобы поправить эту и подобные ситуации в будущем?

Имея эти ответы уже можно думать о том делать ли private fix (только для этого заказчика), public fix (для всех), учесть это проблему и вписать ее в release notes (defer) или же внести в планы будущих релизов полноценную фичу по комплексному устранению этой проблемы (feature).

Цена у всех этих решении разная.

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

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

пятница, 24 сентября 2010 г.

C#: Cash data by time

Часто возникает ситуация когда надо хранить кэш данных, к которым наиболее часто обращаются. Стандартной реализации такой штуки в .NET я не нашел, вот и решил сам поупражняться.
    public class TimeCashDictionary<TKey, TValue> : IDictionary<TKey,TValue>
    {
        protected class TimedValue
        {
            public TimedValue(TValue value)
            {
                LastAccess = DateTime.UtcNow;
                Value = value;
            }
            public TValue Value {get;set;}
            public DateTime LastAccess { get; set; }
        }

        Dictionary<TKey, TimedValue> m_Dict;
        int m_CashSize;
        public int CashSize { get { return m_CashSize; } }

        public TimeCashDictionary(int cashSize)
        {
            m_CashSize = cashSize;
            m_Dict = new Dictionary<TKey, TimedValue>(cashSize+1);
        }

        private void RemoveOldWhenNeed()
        {
            if (m_Dict.Count >= m_CashSize)
            {
                m_Dict.Remove(m_Dict.OrderBy(kv => kv.Value.LastAccess).First().Key);
            }
        }
           
        public void Add(TKey key, TValue value)
        {
            RemoveOldWhenNeed();
            m_Dict.Add(key, new TimedValue(value));
        }

        public bool ContainsKey(TKey key)
        {
            return m_Dict.ContainsKey(key);
        }

        public ICollection<TKey> Keys
        {
            get { return m_Dict.Keys; }
        }

        public bool Remove(TKey key)
        {
            return m_Dict.Remove(key);
        }

        public bool TryGetValue(TKey key, out TValue value)
        {
            TimedValue val;
            var res = m_Dict.TryGetValue(key,out val);
            value = res?val.Value:default(TValue);
            if (res)
                val.LastAccess = DateTime.UtcNow;

            return res;
        }

        public ICollection<TValue> Values
        {
            get
            {
                return  new List<TValue>(m_Dict.Values.Select(el => el.Value));
            }
        }

        public TValue this[TKey key]
        {
            get
            {
                var val = m_Dict[key];
                val.LastAccess = DateTime.UtcNow;
                return val.Value;
            }
            set
            {
                RemoveOldWhenNeed();
                m_Dict[key] = new TimedValue(value);
            }
        }

        public void Add(KeyValuePair<TKey, TValue> item)
        {
            RemoveOldWhenNeed();
            (m_Dict as ICollection<KeyValuePair<TKey, TimedValue>>).Add(new KeyValuePair<TKey, TimedValue>(item.Key, new TimedValue(item.Value)));
        }

        public void Clear()
        {
            m_Dict.Clear();
        }

        public bool Contains(KeyValuePair<TKey, TValue> item)
        {
            return (m_Dict as ICollection<KeyValuePair<TKey, TimedValue>>).Contains(new KeyValuePair<TKey, TimedValue>(item.Key, new TimedValue(item.Value)));
        }

        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            throw new NotImplementedException();
        }

        public int Count
        {
            get { return m_Dict.Count; }
        }

        public bool IsReadOnly
        {
            get { return (m_Dict as ICollection<KeyValuePair<TKey, TimedValue>>).IsReadOnly; }
        }

        public bool Remove(KeyValuePair<TKey, TValue> item)
        {
            return (m_Dict as ICollection<KeyValuePair<TKey, TimedValue>>).Remove(new KeyValuePair<TKey,TimedValue>(item.Key,new TimedValue(item.Value)));
        }

        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            return m_Dict.Select(kv => new KeyValuePair<TKey, TValue>(kv.Key, kv.Value.Value)).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {

            return m_Dict.GetEnumerator();
        }
    }
 
Используется так:
var tc = new TimeCashDictionary<string, string>(3);

tc["sasha"] = "1";
tc["dima"] = "2";
tc["mike"] = "3";
var  v = tc["sasha"];
tc["katy"] = "4";
tc["max"] = "5";
 

Результатом будет:
sasha,katy,max

Версия сырая и на крутизну использования C# не претендующая, но если вдруг приглянется можете использовать под CC лицензией. :)