Team Foundation Server controlled …ehhm… amBX lights?

Enige tijd geleden heeft mijn collega Stephan Smetsers een fraai artikel gepubliceerd om gloeilampen als stoplichten te schakelen. Maar gloeilampen zijn zo 2008, in 2010 willen we LED verlichting!

Natuurlijk kunnen we in de kerst-uitverkoopjes van een online gadget winkel enkele USB kerstboompjes uitzoeken welke met een programmaatje op het
scherm geschakeld worden, maar we willen de lat hoger leggen. Tegenwoordig heb je fraaie Ambilight flatscreens, dus waarom zouden we minder willen?

ambx lights

Toevallig heeft een Philips spin-off Ambilight naar de PC gebracht  http://www.ambx.com/ . Hierdoor is het mogelijk om het spelen van een PC spel te vervolmaken met geluid, licht, wind en trillingen. Leuk voor de kerstdagen maar als developer denken we verder!

Want toevallig is er ook een SDK beschikbaar http://developer.ambx.com/ zodat we toch niet tijdens de feestdagen uit het raam hoeven te staren. Dus wat moeten we nog snel inkopen en/of downloaden voordat de winkels dicht zijn:

Philips amBX starter kitofPhilips amBX 2.1 premium kit 25 euro bij Computerland http://www.computerland.nl/66 euro bij Paradigit http://www.paradigit.nl/
Visual Studio 2008 Express Gratis te downloaden op http://www.microsoft.com/exPress/
Een amBX Developer account Aan te maken op http://developer.ambx.com/
De amBX SDK 1.1.0 Latest release – 26th February 2009 https://developer.ambx.com/component/attachments/download/6
Een C# wrapper rond de ambxrt.dll http://grazz.com/projects/ambxlib/(of https://developer.ambx.com/forum/viewtopic.php?f=5&t=80 )

Vervolgens bouwen we drie hoofdcomponenten:

TFS Custom MsBuild Task Deze task roept de webservice methodes aan om de status van een build aan te geven. Via parameters is door te geven of een build start of stopt. Tevens is de status door te geven (niet of wel gebuild)
Webservice De webservice krijgt commando’s om de status van de build bij te
houden
WindowsService De windows service controleert regelmatig de status op de webservice en stuurt echt de lampen aan via een proxy klasse.

De webservice moet dus verschillende methodes ondersteunen:

Build X is gestart Flikkerend ambi light effecten
Build X is geslaagd Groene glow, langzaam aan en uit
Build X is niet geslaagd:  build faalde Rode glow, snel aan en uit
Wat is de status vande huidige build? Gelezen door windows service

De opstelling wordt dan als volgt:

ambx architectuur

  • De TFS server heeft de mogelijkheid om de amBX server aan te roepen via de custom MSBuild tasks.
  • De webservice op de amBX Server ontvangt de opdrachten en houdt de status (semafoor) vast.
  • Een Windows Service controleert regematig (met een timer) de build status en stuurt de lampen aan.
  • Een ontwikkelaar is verbaasd omdat zijn briljante code wel heel veel
    briljante kleuren oplevert. Zijn baas maakt een aantekening…

Maak dus eerst een build aan op de TFS Server:

TFS Build

Op de TFS kan dan de bijbehorende TFSBuild.proj aangepast worden:

<?xml version="1.0" encoding="utf-8"?>
<Project>
  <UsingTask TaskName="AmbxLighterTask.AmbxTask"
             AssemblyFile="C:\BuildOutput\AmbxLighterTaskLibrary.dll"/>
  <Target Name="BeforeEndToEndIteration">
    <AmbxLighterTask.AmbxTask
      Status="1"
      TfsAmbxWebService="htt p://AmbxServer/TfsAmbxLighter/Service.svc"
      ContinueOnError="true"/>
  </Target>
  <Target Name="AfterEndToEndIteration">
    <AmbxLighterTask.AmbxTask
      Status="2"
      TfsAmbxWebService="htt p://AmbxServer/TfsAmbxLighter/Service.svc"
      ContinueOnError="true"/>
  </Target>
  <Target Name="BeforeOnBuildBreak">
    <AmbxLighterTask.AmbxTask
      Status="3"
      TfsAmbxWebService="htt p://AmbxServer/TfsAmbxLighter/Service.svc"
      ContinueOnError="true"/>
  </Target>
</Project>

Check deze weer in als deze aangepast is.

Plaat vervolgende op de opgegeven lokatie op de TFS server de dll met
de custom MSBuild task (AmbxLighterTaskLibrary.dll)

public class AmbxTask : Task
{
  [Required]
  public string Status { get; set; }
  [Required]
  public string TfsAmbxWebService { get; set; }
  public override bool Execute()
  {
    try
    {
      SemaphoreType semaphore =
        (SemaphoreType)Convert.ToInt32(Status);
      BasicHttpBinding basicHttpBinding = new BasicHttpBinding();
      EndpointAddress endpointAddress = new
        EndpointAddress(TfsAmbxWebService);
      ChannelFactory<IService> channelFactory =
        new ChannelFactory<IService>(basicHttpBinding,
          endpointAddress);
      IService service = channelFactory.CreateChannel();
      switch (semaphore)
      {
        case SemaphoreType.BuildIsStarted:
          service.BuildHasStarted();
        break;

        case SemaphoreType.BuildIsSucceeded:
          service.BuildHasSucceeded();
        break;

        case SemaphoreType.BuildIsFailed:
          service.BuildHasFailed();
        break;
      }

      channelFactory.Close();
      return true;
    }
    catch (Exception ex)
    {
      Log.LogMessage("*** AmbxTask " + Status
          + " exception: " + ex.Message);
      return false;
    }
  }
}

In de task word een aanroep naar de AmbxServer uitgevoerd (geen extra config
nodig met WCF settings voor de connectie in dit geval).

De WCF service heeft het volgende contract:

[ServiceContract]
public interface IService
{
  [OperationContract]
  void BuildHasStarted();
  [OperationContract]
  void BuildHasSucceeded();
  [OperationContract]
  void BuildHasFailed();
  [OperationContract]
  int ShowStatus();
}

De service draait dus op de Ambx server en is stateless met uitzondering van
één STATUS waarde. Deze wordt met een WindowsService timer uitgelezen:

void _Timer_Elapsed(object sender, ElapsedEventArgs e)
{
  TfsAmbxLighterServiceReference.ServiceClient
    client =
      new TfsAmbxLighterServiceReference.ServiceClient();
  SemaphoreType semaphore = (SemaphoreType) client.ShowStatus();
  switch (semaphore)
  {
    case SemaphoreType.BuildIsStarted:
      ResetTeller();
      AmbxProxy.ShowBuilding();
    break;

    case SemaphoreType.BuildIsSucceeded:
      _Teller++;
      if (_Teller >= _IdleInterval)
      {
        if (_Teller % _IdleInterval == 0)
        {
          AmbxProxy.ShowBuildSucceed();
          break;
        }
        break;
      }
      AmbxProxy.ShowBuildSucceed();
    break;

    case SemaphoreType.BuildIsFailed:
      _Teller++;
      if (_Teller >= _IdleInterval)
      {
        if (_Teller % _IdleInterval == 0)
        {
          AmbxProxy.ShowBuildFailed();
          break;
        }
        break;
      }
      AmbxProxy.ShowBuildFailed();
    break;
  }
  client.Close();
}

En de windowsService roept du s een Proxy klasse rond de amBX api aan (met
gebruik van amBXLib van http://grazz.com/projects/ambxlib/

public class AmbxProxy
{
  public static void ShowBuildSucceed()
  {
    using (amBX engine = new amBX(1, 0, "AmbxProxy", "v1.0"))
    {
      amBXLight everyLight =
        engine.CreateLight(
          CompassDirection.Everywhere,
          RelativeHeight.AnyHeight);
      everyLight.FadeTime = 1800;
      everyLight.Color = new amBXColor(){Red=0f,Green=1f,Blue=0f};
      Thread.Sleep(1800);
      everyLight.Color = new amBXColor(){Red=1f,Green=1f,Blue=1f};
      Thread.Sleep(1800);
      everyLight.Color = new amBXColor(){Red=0f,Green=0f,Blue=0f};
      Thread.Sleep(1800);
    }
  }
}

De amBXLight instantie wordt dus steeds van een andere kleur voorzien
en de opgegeven fade krijgt de kans door de blocked call (de sleep implementatie). Het is prettig voor de gebruiker als je weer terug fade naar zwart.

Het resultaat is hier te bekijken op YouTube waarbij ik de  werking demonstreer met een laptop en een  server.

Conclusie

Voor een paar centen  heb je een hoop lol en leer je terloops ook nog iets over TFS uitrollen en  inrichten. Ook WCF Webservice, Windows Services en MSBuild komt langs. Dus je hoeft je niet te vervelen. Mocht het toch niks worden met TFS dan kun je altijd nog de kerstperiode vullen met het bijgeleverde racespel.

Prettige kerstdagen!

Update: Met TFS 2010 stapt MS over op Workflow Foundation (WF4) als build script. Hoewel bestaande MSBuild oplossingen ook nog ondersteund worden zou bij WF4 gekozen kunnen worden voor het simpel toevoegen van een SendActivity. Deze al bestaande Activity kan WCF services aanroepen. De AmbxTask verandert in een geconfigureerde activity. Dus kan gewoon de juiste methode op de http://AmbxServer/TfsAmbxLighter/Service.svc aangeroepen
worden.

Set up een eigen Setup

Het in Visual Studio aanwezige installer Setup project is om de één of
andere reden niet populair bij ontwikkelaars. Het wordt vaak als te beperkt
gezien. Volgens mij heeft dit gewoon te maken met de leercurve, Microsoft is er
goed in geslaagd de best uitgebreide functionaliteit goed te verbergen achter
vage iconen en iets te veel dialoogjes.

Enige tijd geleden heb ik al eens geblogged over het conditioneel uitvoeren van Setup logica.

Droste Effect

Nu wil ik stilstaan bij een ander redelijk onbegrepen stukje gereedschap:
Custom installers. Hiermee is het mogelijk om een redelijk standaard setup op te
schalen tot een uitgebreide setup door eigen code uit te voeren. Dat dit een
krachtige uitbreiding is, komt door de mogelijke interactie tussen de standaard
Setup en de eigen code.

Custom actions

We gaan dus een Setup uitbreiden met custom actions. Start eerst een
winform project en voeg daar een Setup project aan toe. We hebben nu twee
projecten in één solution:

P01

Voeg de output van de WinForm applicatie toe aan de setup:

P02

De rebuild van de totale solution is niet voldoende. Hercompileer en build de
setup expliciet. Zie dat de Setup succesvol gebouwd wordt:

P03

We gaan nu de Custom Installer toevoegen. Maak eerst een standaard
Library project aan:

P04

Verwijder de aanwezige klasse en voeg een Installer class toe:

P05

Override de methodes Install, Commit, Rollback en Uninstall. Plaats in de
methodes een MessageBox.Show (met dank aan de System.Windows.Forms
namespace):

namespace ClassLibrary
{
  [RunInstaller(true)]
  public partial class Installer :
  System.Configuration.Install.Installer
  {
    public Installer()
    {
      InitializeComponent();
    }

    public override void Install(IDictionary stateSaver)
    {
      base.Install(stateSaver);
      MessageBox.Show("Install");
    }
    public override void Commit(IDictionary savedState)
    {
      base.Commit(savedState);
      MessageBox.Show("Commit");
    }
    public override void Rollback(IDictionary savedState)
    {
      base.Rollback(savedState);
      MessageBox.Show("Rollback");
    }

    public override void Uninstall(IDictionary savedState)
    {
      base.Uninstall(savedState);
      MessageBox.Show("Uninstall");
    }
  }
}

Hercompileer de solution en fix eventuele fouten. Voeg hierna ook de
primary output van de library toe aan de Setup.

P06

Het enige wat nu nog gedaan moet worden, is het koppelen van de Custom
installer class aan de setup. Open dus in het Setup project de Custom Actions
Editor. Je ziet hier dezelfde mogelijke custom actions (Install, Commit,
Rollback en Uninstall).

P07

Er is nu nog geen Custom Action gekoppeld dus daar gaan we veranderingen
in aanbrengen. Selecteer (de root van de) Custom Actions en kies voor Add Custom Action via een rechter-muis-klik. Selecteer nu de primary output van de
Classlibrary (want hierin zit de Installer Class).

P08

Zie dat in één keer alle vier de actions toegevoegd zijn:

P09

Hercompileer nu het Setup project en start de installatie. Zie dat
tijdens de tweede helft van de installatie onze dialogen getoond worden. Hier
had dus uw eigen logica uitgevoerd kunnen worden

P10

P11   en 

Hierna is de setup compleet uitgevoerd. De-installeer nu de
applicatie en zie dat ook de UnInstall Action afgaat:

P13

Debuggen

Het is nu interessant om eens naar het debuggen te kijken. De
eenvoudigste manier zou een BreakPoint zijn. Helaas werkt dit niet voor een
Setup project. En “Attach to Process” zou ook een optie kunnen zijn maar dat is
best wel bewerkelijk. Het is eenvoudiger om via code een breakpoint uit te
voeren.

System.Diagnostics.Debugger.Break();

Hierna zal bij het uitvoeren van de ‘breakpoint’ gevraagd worden om
Visual Studio te starten en kan de code de uitvoer alsnog gedebugged worden.
Vergeet deze achteraf niet te verwijderen!

P14

Rollback

Mocht tijdens de install nu toch gekozen worden om de totale
installatie terug te draaien, creëer dan een InstallerException.

public override void Install(IDictionary stateSaver)
{
  base.Install(stateSaver);
  throw new InstallException("Helaas...");
  //MessageBox.Show("Install");
}

Hiermee zal de opgegeven tekst getoond worden, en zal de rollback uitgevoerd
worden. De setup is hierna teniet gedaan. Zorg er dus voor dat al eventueel
uitgevoerde logica ook verwijderd wordt.

P15   P16

Doorgifte parameters

Het is tevens mogelijk om gegevens door te geven aan de custom
installer. Pas hiervoor de Custom Action aan.

Eerst moeten we de doorgifte instellen. Vul daarvoor de CustomActionData in.
Als voorbeeld geef ik hier de TargetDir door, dat is de map waarin de applicatie
geïnstalleerd wordt. Het let overigens heel nauw hoe deze doorgegeven worden.
Let dus goed op de back-slash en de quotes.

P17

Daarna moeten we de code op de doorgegeven waarde laten
reageren:

public override void Install(IDictionary stateSaver)
{
  base.Install(stateSaver);
  string value = Context.Parameters["TargetDir"];
  if (!string.IsNullOrEmpty(value))
  {
    MessageBox.Show("Install " + value);
  }
  else
  {
    MessageBox.Show("Install --");
  }
}

Nu zal de map doorgegeven worden en kan hier dus extra op gereageerd worden met eigen code.

P18

Kijk hier voor een voorbeeld waarin uitgelegd wordt hoe ook tekst doorgegeven kan worden.

Conclusie

In enkele minuten is een vrij statische installatie veranderd in een
weelderige Setup waarin eigen code uitgevoerd kan worden. Hierdoor zijn er geen grenzen meer zijn aan de mogelijkheden om de gebruiker met succes te begeleiden tijdens de installatie en de de-installatie.