Søger du en specifik kategori?

 



Oprettet tor. d. 05. februar 2009 kl. 10:47

nielle
nielle (159.511 point)
Guidens karaktér
1
2
3
4
5
Mangler vurderinger

Windows services med .NET

Artiklen viser hvordan du kan lave en Windows service i .NET. Services bruges til at udføre rutine opgaver som ikke kræver overvågning. Windows services kan køre uden at der er logget nogen ind – blot PC'en er startet.
Indledning

En Windows service er et program, som kører uovervåget i baggrunden af din Windows. En typisk brug af dem er til at overvåge et eller andet eller til at lade servicen fungere som server.

Et par kendte services er IIS og SQL Server 2005, og i det hele taget ser man oftes services brugt som servere.

Artiklen viser hvordan man selv laver en service vha. C# og .NET og hvordan man administrerer den.

Services har ingen GUI - de køre som sagt totalt uovervåget i baggrunden. Når de har brug for at meddele sig til omverdenen foregår dette sædvanligvis ved at de skriver i Windows' event log. Artiklen giver derfor et eksempel på hvordan dette gøres.

Da jeg selv pt. stadig arbejder i .NET 2.0 er eksemplerne givet ud fra VS 2005. Jeg har dog ingen grund til at antage at det skulle være væsentligt anderledes i den netop frigivne 2008.

Artiklen kræver Visual Studio 2005 Professionel eller bedre for at eksemplerne kan eftergøres. Projekt skabelonen for Windows Services findes ikke i mindre versioner.

v. 1.0: 08/01/2008 - Første version.


Demo applikationen

I det følgende laver jeg en lille demo applikation, som holder øje med vores alle sammen yndling website og tjekker efter om det er oppe.

En mulig variation på denne applikation er at den automatisk, og med jævne mellemrum, kan kalde et url på dit eget website for dermed få udført en opgave. Det forudsætter selvfølgelig at din egen maskine er tændt.

Hvis man har direkte adgang til serveren som hoster ens website, ville en mere direkte løsning være at lave en Windows service som køre en program stump, som udføre arbejdet direkte. Det er svarer til at køre et cronjob på en Linux installation.


Vores Windows service

Start med at lave et Windows Service projekt i Visual Studio og kald projektet for "WebSiteMonitor".

Det første man bliver præsenteret for er designeren for klassen "Service1". Da en Windows service ikke har noget GUI er denne grå, tom, kedelig, og det eneste den indeholder, er teksten:

"To add components to your class, drag them from the _Toolbox_ and use the Properties window to set their properties. To create methods and events for your class, _click_here_to_switch_to_code_view_."

Under properties ændres værdien af ServiceName fra "Service1" til "WebSiteMonitor". Hvis man ikke lige kan se denne, så skal man først klikke et sted på den grå designer.

Man kan også vælge at ændre Name til "WebSiteMonitor" på dette tidspunkt, men dette er mere af kosmetisk betydning og har ikke nogen reel konsekvens - det svare til at omdøbe en Windows forms fra "Form1" til f.eks. "ForsideForm".

ServiceName er det navn servicen kendes under i selve operativsystemet. Man starter det f.eks. fra kommando linjen med denne:

prompt> net start WebSiteMonitor


Klik på det sidste af de to links i designer-vinduet. Man bliver herefter præsenteret for skelettet til en Windows service.

namespace WebSiteMonitor
{
    public partial class Service1 : ServiceBase
    {
        public Service1()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            // TODO: Add code here to start your service.
        }

        protected override void OnStop()
        {
            // TODO: Add code here to perform any tear-down necessary to stop your service.
        }
    }
}


Som minimum skal eventhandlerene OnStart() og OnStop() udfyldes.

Man kan eventuelt vælge at overstyre et par stykker mere end dette minimum:

(o) OnContinue(),
(o) OnPause() og
(o) OnShutdown()

For at servicen vil tillade at man kalde disse, skal Service1's to properties, CanPauseAndContinue og CanShutdown, samtidig ændres fra at være false til true.

Som i enhver anden eventhandler bør koden i OnStart() og OnStop() være "hit'n'run". Det må ikke være noget som tager lang tid om at køre færdig, for ellers kan det hænge operativsystemet i mellemtiden. I stedet skal koden blot sætte arbejdet i gang og ellers lade noget andet kode om at udføre jobbet.

Hvis man f.eks. ønsker at monitorer om Eksperten.dk er oppe og kører, overlades dette bedst til en timer som står og tjekker med et passende mellemrum (maks. 30 sekunder). I OnStart() og OnStop() hhv. startes og stoppes timeren.

Timeren initialiseres i servicens constructor, og får her bl.a. hægtet en eventhandler på til at tage sig af hver gang at timeren "tikker". På de tidspunkter skal den forsøge at kontakte Eksperten.dk for at se om dette kan lade sig gøre.

Med lidt ekstra finesser bliver det samlet til noget i denne stil:

using System;
using System.Diagnostics;
using System.ServiceProcess;
using System.Timers;
using System.Net;
using System.IO;

namespace WebSiteMonitor
{
    public partial class Service1 : ServiceBase
    {
        // Vores favorit site.
        private const string url = "http://www.eksperten.dk";

        // Til Windows' event log.
        private const string eventLogSourceName = "EkspertenMonitor";

        // Timer til at pinge Eksperten.
        Timer timer;

        public Service1()
        {
            InitializeComponent();

            if (!EventLog.SourceExists(eventLogSourceName))
                EventLog.CreateEventSource(eventLogSourceName, "Application");

            timer = new Timer(10 * 60 * 1000);  // Hvert 10. minut
            timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
        }

        void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            try
            {
                // Kontakt websitet.
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();

                // Skriv resultatet til log-filen.
                string logPath = @"C:\WebSiteMonitor.log";
                using (StreamWriter log = new StreamWriter(logPath, true))
                {
                    string logMsg = string.Format("Status {0} for {1} : {2}",
                    DateTime.Now.ToString(), url, response.StatusCode.ToString());
                    log.WriteLine(logMsg);
                }

                // Luk for response-streamen.
                response.Close();
            }
            catch (Exception ex)
            {
                string eventMsg = string.Format("Exception: {0}", ex.Message);
                EventLog.WriteEntry(eventLogSourceName, eventMsg, EventLogEntryType.Warning);
            }
        }

        protected override void OnStart(string[] args)
        {
            timer.Start();
            EventLog.WriteEntry(eventLogSourceName, "Monitorering af Eksperten.dk startet", EventLogEntryType.Information);
        }

        protected override void OnStop()
        {
            timer.Stop();
            EventLog.WriteEntry(eventLogSourceName, "Monitorering af Eksperten.dk startet", EventLogEntryType.Information);
        }
    }
}


En af de omtalte finesser er at servicen skriver til Windows' event log. Da en service ikke har nogen GUI er det meget almindeligt at de kommunikere med brugeren eller administratoren ved at skrive i netop event loggen. De har f.eks. ikke nogen mulighed for at poppe op med en fejl dialog.


Installer'en

Selvom koden faktisk kompilere til en exe-fil, kan denne ikke køre. Hvis man alligevel prøver, bliver man mødt med følgende fejlbesked:

"Tjenesten kan ikke startes fra kommandolinjen eller et fejlfindingsprogram. Du skal først bruge installutil.exe til at installere en Windows-tjeneste, der dernæst skal startes med Server Explorer, administrationsværktøjet til Windows-tjenester eller kommandoen NET START."

Windows services afviger fra almindelige programmer ved at man skal lave en installer. Installeren har til formål at installere og ikke mindst konfigurere hvordan Windows servicen skal køre.

Af en eller anden grund er menuen til at oprette installeren lidt godt skjult... Man stiller sig på designvinduet for Service1 (den grå kedelige side fra starten). Højre-klik på denne. Dette bringer en lille kontekst menu frem. Vælg Add Installer. Der skulle nu gerne optræde to nye komponenter "serviceProcessInstaller1" og "serviceInstaller1".

Markér først serviceInstaller1 og ret så følgende i dens properties:

(o) Description sættes til "Monitorere om Eksperten.dk's hjemmeside er oppe".
(o) DisplayName sættes til "WebSite Monitor".
(o) StartType sættes til Automatic.

Der er værdierne i Description og DisplayName, som vises når man administrere servicen fra kontrolpanelets "Tjenester" ("Services" på en engelsk Windows installation). Mere om det senere.

"Automatic" betyder at servicen, når den er installeret, automatisk vil starte op sammen med Windows. Man kan desuden vælge Manual og Disabled, hvor den første betyder at man selv skal starte servicen via Tjenester eller fra en kommando prompt.

NB: Indtil at man er færdig med sin service og har fået gennemtestet den grundigt, er det som regel en !! rigtig !! god idé at vælge "Manual". Hvis servicen af en eller anden grund får maskinen til at hænge og den er sat til at starte Automatisk med operativsystemet, ender man i det værste tilfælde med en maskine som det kan være meget svært at komme i kontakt med igen!

Bemærk ar servicen i øvrigt sagtens kan startes med operativsystemet uden at der faktisk behøver at være nogen som logger ind på maskinen.

Markér dernæst serviceProcessInstaller1 og ret så følgende i dens properties:

(o) Account sættes til LocalSystem

De mulige værdier er:

(o) LocalService : En konto som fungere som en non-priviligeret bruger på den lokale computer, og som præsentere sig med anonyme credentials til eventuelle fjern-servere.
(o) NetworkService : En konto som giver udvidede lokale privilegier, og som har lov til at præsentere sig computerens credentials for at logge ind på fjern-servere. Ikke nødvendigvis krævet for at kalde f.eks. en webserver.
(o) LocalSystem : En konto med næsten ubegrænsede lokale privilegier, og som har lov til at præsentere sig computerens credentials for at logge ind på fjern-servere. Bruges af service kontrolleren.
(o) User : En konto tilhørende en bestemt bruger på netværket. Bruger man denne skal der suppleres med login oplysninger under installationen.

Ideelt bør man helt ikke bruge LocalSystem da eventuelle sikkerhedshuller i en Windows service da potentielt kan bruges til at kompromittere operativsystemet. Her bruger jeg den fordi at den giver rettighed til at skrive i filsystemet. Dette kunne dog også klares med oprettelse af en brugerkonto med det specifikke privilegium, og ikke meget andet, og så køre den under User.

Kompilér til sidst projektet.


Manuel installation

Nu skal servicen installeres sådan at vi kan se den i aktion. Til det skal vi bruge programmet InstallUtil.exe, som kommer sammen med .NET frameworket.

Start en kommando prompt. Først navigerer vi hen til det sted hvor den kompilerede service ligger. Hos mig kan det gøres med disse to kommandoer:

prompt> cd \
prompt> cd Source.Net20\MonitorEksperten\bin\Release


Man kan evt. kontrollere efter med:

prompt> dir


- for at se om man er det sted hvor WebSiteMonitor.exe ligger.

Derefter installeres Windows servicen med InstallUtil.exe.

prompt> C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe WebSiteMonitor.exe


Her har jeg brugt versionen af InstalUtil.exe fra .NET 2.0. Hvis du selv bruger en anden .NET version skal du bruge det rigtige bibliotek i stedet for v2.0.50727.

Man afinstallere i øvrigt med denne variation på kommandoen:

prompt> C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe /u WebSiteMonitor.exe



Efter installationen

Nu er servicen installeret og den vil tilmed automatisk starte op næste gang at PC'en startes. Man kan dog også starte den med det samme. Dette gøres f.eks. via kontrolpanelet:

Start-knappen > Indstillinger > Kontrolpanel > Administration > Tjenester

("Tjenester" i en dansk Windows, "Services" i en engelsk)

I listen skulle der gerne optræde en service som hedder "WebSite Monitor". Man kan starte og stoppe den ved at højre-klikke på servicen og vælge Start hhv. Stop fra den resulterende kontekst menu.

Man kan endvidere følge med i servicens arbejde via Windows' Event Log.:

Start-knappen > Indstillinger > Kontrolpanel > Administration > Logbog

Kig under "Programmer" - her skulle der gerne stå to beskeder (hvis du har prøvet at starte servicen). Den ene er fra "EkspertenMonitor" og indeholder beskeden "Monitorering af Eksperten.dk startet". Den anden er fra "WebSiteMonitor" og indeholder beskeden "Tjenesten blev startet.".

Man behøver altså strengt taget ikke selv skrive at servicen starter og stopper. At den gør dette skyldes i øvrigt at servicens AutoLog er sat til true pr. default.


GUI til servicen

Som sagt har en service ikke nogen GUI. Strengt taget er dette ikke 100 % korrekt. Den har en GUI i sin egen inaktive Windows Station. Da den er inaktiv er den bare ikke synlig, og alle dialoger og fejlmeddelelser man måtte finde på at prøve at vise vil derfor også være usynlige. I øvrigt ville det ikke være muligt at betjene dem via keyboard eller mus alligevel.

Hvis man alligevel forsøger at vise dialoger, ender man blot med en service som muligvis hænger fordi at den venter på input fra brugeren.

Den sædvanlige løsning på dette problem er at konstruere servicen som en server og brugegrænsefladen som en klient til denne server. Der findes mange forskellige måde man kan lave en client/server applikation på; i det næste eksempel sættes servicen f.eks. op med et webserver interface sådan at den kan tilgås via en almindelig Internet browser.


Demo applikation #2

Denne overvåger det bibliotek hvor MSIE lægger sine temporære filer. Ved at følge med i dette bibliotek, kan man danne sig et billede af hvad der egentlig sker når man sidder og surfer.

Da der er noget mere kompleksitet i dette tilfælde er servicen opdelt i tre klasser: selve servicen, dens "last" (klassen som overvåger filsystemet) og webinterfacet.


Demo #2: Servicen

Først og fremmest har vi selve den afledte ServiceBase klasse. Den har ansvar for at starte og stoppe servicen. Det gør den ved at starte og stoppe Payload- og WebInterface-instanserne. Desuden stiller den en log-funktion til rådighed for dem som måtte ønske at skrive til event loggen.

using System.Diagnostics;
using System.ServiceProcess;

namespace InternetActivity
{
    public partial class InternetActivityService : ServiceBase
    {
        private const string eventLogSourceName = "InternetActivityService";

        internal Payload payload;
        private WebInterface webInterface;

        public InternetActivityService()
        {
            InitializeComponent();

            if (!EventLog.SourceExists(eventLogSourceName))
                EventLog.CreateEventSource(eventLogSourceName, "Application");

            payload = new Payload(this);
            webInterface = new WebInterface(this);
        }

        protected override void OnStart(string[] args)
        {
            payload.Start();
            webInterface.Start();
        }

        protected override void OnStop()
        {
            payload.Stop();
            webInterface.Stop();
        }

        internal void Log(string message, EventLogEntryType type)
        {
            EventLog.WriteEntry(
                eventLogSourceName,
                message,
                System.Diagnostics.EventLogEntryType.Error);
            }
        }
    }
}



Demo #2: Payload

Payload-klassen indeholder hjertet af servicen. Det er den som har ansvaret for at overvåge biblioteket hvor temporære Internet filer gemmes (browser cachen). "Payload" betyder "last" på dansk.

Dette gøres ved at opsætte en FileSystemWatcher og abonnere på de events der rejses når der oprettes og slettes filer i biblioteket.

Hændelserne opsummeres i en lille statistik, som så kan udskrives på forlangende (af WebInterfacet i dette tilfælde). Jeg har valgt at opsummer det på extension-niveau.


using System;
using System.IO;
using System.Collections.Generic;

namespace InternetActivity
{
    internal class Payload
    {
        private InternetActivityService service;

        private FileSystemWatcher watcher;

        struct StatKey
        {
            public string fileExtension;
            public string eventType;
        }

        Dictionary<StatKey, int> stats;

        internal Payload(InternetActivityService service)
        {
            this.service = service;

            string temporaryInternetFiles =
                Environment.GetFolderPath(Environment.SpecialFolder.InternetCache);

            // Opret watcheren.
            watcher = new FileSystemWatcher(temporaryInternetFiles);
            watcher.IncludeSubdirectories = true;
            watcher.EnableRaisingEvents = false;

            // Sæt watcheren til at lytte på hændelser i filsystemet.
            watcher.Changed += new FileSystemEventHandler(fsw_Changed);
            watcher.Created += new FileSystemEventHandler(fsw_Created);
            watcher.Deleted += new FileSystemEventHandler(fsw_Deleted);
            watcher.Renamed += new RenamedEventHandler(fsw_Renamed);

            // Fejlhåndtering.
            watcher.Error += new ErrorEventHandler(fsw_Error);

            ResetStats();
        }

        #region Start/stop the watcher.

        internal void Start()
        {
            watcher.EnableRaisingEvents = true;
        }

        internal void Stop()
        {
            watcher.EnableRaisingEvents = false;
        }

        #endregion

        #region File system events

        private void fsw_Changed(object sender, FileSystemEventArgs e)
        {
            UpdateStats("Changed", e.Name);
        }

        private void fsw_Created(object sender, FileSystemEventArgs e)
        {
            UpdateStats("Created", e.Name);
        }

        private void fsw_Deleted(object sender, FileSystemEventArgs e)
        {
            UpdateStats("Deleted", e.Name);
        }

        private void fsw_Renamed(object sender, RenamedEventArgs e)
        {
            UpdateStats("Renamed", e.Name);
        }

        #endregion

        #region Statistikken

        internal void ResetStats()
        {
            stats = new Dictionary<StatKey, int>();
        }

        private void UpdateStats(string eventType, string fileName)
        {
            string fileExtension = Path.GetExtension(fileName).ToLower();

            // Enten findes den allerede i statistikken ...
            foreach (StatKey key1 in stats.Keys)
            {
                if (key1.fileExtension == fileExtension && key1.eventType == eventType)
                {
                    // ... og så skal den bare opdateres ...
                    stats[key1]++;
                    return;
                }
            }

            // ... eller også er den helt ny, og så skal den oprettes.
            StatKey key2 = new StatKey();
            key2.fileExtension = fileExtension;
            key2.eventType = eventType;
            stats[key2] = 1;
        }

        internal string[] GetStats()
        {
            List<string> result = new List<string>();

            foreach (StatKey key in stats.Keys)
            {
                string formattedStat = string.Format("{0} ({1}) : {2}", key.fileExtension, key.eventType, stats[key]);
                result.Add(formattedStat);
            }

            string[] resultArr = result.ToArray();
            Array.Sort(resultArr);
            return resultArr;
        }

        #endregion

        #region Fejl håndtering.

        private void fsw_Error(object sender, ErrorEventArgs e)
        {
            service.Log(
                string.Format("Exeption: {0}", e.GetException().ToString()),
                System.Diagnostics.EventLogEntryType.Error
            );
        }

        #endregion
    }
}


For at dette overhovedet vil virke, skal servicen i dette tilfælde installeres under User-kontoen. Ellers vil den overvåge et helt andet - og temmelig uinteressant - bibliotek, end det som hører sammen med din bruger-konto.

Når du kommer så langt som til at skulle til at installere servicen bliver du bedt om at indtaste login-oplysninger for User-kontoen. I feltet hvor der står Navn er det i virkeligheden:

<maskin-navn>\<login-navn>

- som skal indtastes. Ellers får man blot at vide at der ikke findes nogen konto med de oplysninger.


Demo #2: Web interfacet

Servicen er udstyret med sin egen lille indbyggede web server. Dette betyder at man kan kontakte den via en almindelig browser. F.eks giver URL'et:

http://localhost:1066/ (...)

- en opsummering af de stats som Payload-objektet opsamler.

Web interfacet kan også bruges til at resette statistikken.

using System.IO;
using System.Net;
using System.ServiceProcess;
using System.Threading;

namespace InternetActivity
{
    class WebInterface
    {
        private InternetActivityService service;

        private Thread listenerThread;
        private HttpListener listener;

        public WebInterface(InternetActivityService service)
        {
            this.service = service;

            // Lyt på port 1066.
            listener = new HttpListener();
            listener.Prefixes.Add("http://localhost:1066/ (...));
        }

        internal void Start()
        {
            listener.Start();

            listenerThread = new Thread(Listen);
            listenerThread.Start();
        }

        internal void Stop()
        {
            listenerThread.Abort();
            listener.Stop();
        }

        private void Listen()
        {
            bool keepGoing = true;
            while (keepGoing)
            {
                // Vent på at en klient forespørger.
                HttpListenerContext context = listener.GetContext();

                // Svar på forespørgelsen.
                HttpListenerRequest request = context.Request;
                HttpListenerResponse response = context.Response;

                using (StreamWriter sw = new StreamWriter(response.OutputStream))
                {
                    switch (request.Url.AbsolutePath)
                    {
                        case "/service/reset":
                            sw.WriteLine("Resetting stats.");

                            service.payload.ResetStats();

                            break;
                        case "/service/stats":
                            string[] statArr = service.payload.GetStats();
                            string stats = string.Join("<br>", statArr);

                            sw.WriteLine("Stats:<br>");
                            sw.WriteLine(stats);

                            break;
                        default:
                            sw.WriteLine("Ukendt kommando.");
                            break;
                    }

                    sw.Close();
                }
            }

            listener.Stop();
            listener.Close();
        }
    }
}



Gi' den en tur

En lille tur omkring Eksperten, efter man har tømt sin cache, kan hurtigt komme til at se ud som noget i stil med dette:

Stats:
(Changed) : 144
(Created) : 5
(Deleted) : 8
<snip>
.css (Changed) : 2
.css (Created) : 2
.css (Deleted) : 1
.dat (Changed) : 2
.gif (Changed) : 29
.gif (Created) : 29
.gif (Deleted) : 13
.htm (Changed) : 20
.htm (Created) : 20
.htm (Deleted) : 15
.ico (Changed) : 2
.ico (Created) : 2
.ico (Deleted) : 1
.ie5 (Changed) : 13
.ini (Changed) : 5
.ini (Created) : 5
.ini (Deleted) : 5
.js (Changed) : 19
.js (Created) : 19
.js (Deleted) : 10
.png (Changed) : 4
.png (Created) : 4
.png (Deleted) : 3
.swf (Changed) : 7
.swf (Created) : 7
.swf (Deleted) : 2


Lidt tankevækkende, ikke?


Yderligere læsning

Debugging af Windows services:

http://msdn2.microsoft.com/ (...)

.oOo.

.NET FRAMEWORK 2.0
Application Development Foundation
Microsoft
ISBN-13: 978-0-7356-2277-7


Chapter 8, Lesson 3 giver en grundlæggende indføring i hvordan man laver en Windows service.

.oOo.

Professionel C# 2005
Wrox
ISBN-13: 978-0-7645-7334-1


Chapter 36 giver en god og dybdegående gennemgang af hvordan en Windows service fungerer.

Skriv en kommentar



Mest populære guides

Guidens karakter
!!!Karaktér: 3
14 stemmer
31/01 - 2011
Af: heinzdmx

Dropbox - gratis online lagerplads

Jeg vil i denne guide forklare lidt om hvad Dropbox er og også hvordan du får mest mulig plads på Dropbox. Dropbox er kort sagt en service hvor du har dine data lagt til backup på både nettet og din egen computer.
Guidens karakter
!!!Karaktér: 4
33 stemmer
02/02 - 2009
Af: jkrons

Dato- og tidsberegninger i Excel

En introduktion til simple beregninger med dato og tid i Excel. Opdateret med afsnit om beregning af tillæg.
Excel  |  Læs »
Guidens karakter
!!!Karaktér: 4
21 stemmer
06/11 - 2011
Af: fromsej

Sådan fjerner du virus og malware

Udviklingen går stærkt på "skidt"fronten, så vi har sammensat en ny og effektiv programpakke til fjernelse af det.
Virus  |  Læs »

Log ind

   

   



   




Tips & Tricks fra PC World

Teaser billede

Top 5: Virale YouTube-videoer fra Danmark

Lægger du mærke til de mere eller mindre skjulte reklamebudskaber, når du ser videoer på nettet? Vi har taget et kig på fem utrolige danske videoer, som er blevet virale hit.


Anmeldelser fra PC World

Teaser billede

Test: Mobil med Ferrari-design - og en Trabant-motor

Motorola har begået endnu en smartphone med lækkert design og potentiale til at være blandt de bedste. Men den når ikke i mål. Se her hvorfor.


Seneste blogindlæg

Teaser billede

Tvangslukke spørgsmål: Hvad er den bedste løsning?

Hej Vi har mange åbne spørgsmål på Eksperten. Vi ville gerne tvangslukke dem - så et spørgsmål efter f.eks. 6 måneder lukkes. Men der er et par uklarheder som ville være gode at få lidt input til:...


Nyheder fra PC World

Teaser billede

Sådan fupper smarte svindlere dig på Facebook

Se hvordan du undgår Facebook-fup i fremtiden.


Nyheder fra Computerworld

Teaser billede

App-udvikling 2.0: Sådan er den perfekte app

ComputerViews: Den værste app-hype er ved at have lagt sig, og nu ser vi konturerne af fremtidens app-design. Men hvordan udnytter man de mobile muligheder optimalt?


Kurser
Samarbejdspartnere

Udgiver · © 2012 IDG Danmark A/S · Hørkær 18 · 2730 Herlev · Tlf.: 77 300 300 · Fax: 77 300 301 · Brug af personoplysninger