mardi 20 juillet 2010

Installer le Framework .NET sur un poste client via code (sans être administrateur)


Lors d'un projet de migration des applications Winforms du .NET 1.1 vers .NET 3.5, on a réfléchi au moyen de déploiement du Framework .NET3.5 sur les postes d'utilisateurs. Les utilisateurs ne sont pas administrateurs sur les postes. Ils ne peuvent donc pas installer le Framework eux-mêmes. Le déploiement de l'application se fait par ClickOnce via un URL :
http:// [url]. [Domaine].com/[nomapplication].application
Une possibilité était d'inclure le code d'installation de Framework .NET3.5 lors de lancement de l'application.

Les principes :
1. Vérifier si le Framework .NET 3.5 est installé sur le poste client.
2. Si Oui, installer l'application.
3. Si Non, installer d'abord le Framework .NET 3 .5 en utilisant un profil administrateur, puis installer l'application.

Les étapes :
1. J'ai défini dans le fichier de configuration le chemin vers l'exécutable du Framework .NET3.5, le nom ainsi que le mot de passe d'utilisateur qui installera l'application et l'url de l'application.
……..
<appSettings>
<add key= "application" value="http://[URL application]\setup.exe"/>
<add key= "cmd" value="http://[URL application]\setup.exe"/>
<add key= "user" value="[Nom utilisateur]"/>
<add key= "pwd" value="[Mot de passé utilisateur]"/>
<add key= "domain" value="[Domaine utilisateur]"/>
</appSettings>
……
2. J'ai créé une classe qui contient une méthode permettant de lancer un processus sous le contexte d'un utilisateur. J'ai eu le code ci-dessous en cherchant sur internet.

Pour voir plus loin : http://msdn.microsoft.com/en-us/library/ms682431(VS.85).aspx

using System;using Microsoft.Win32;
using System.Runtime.InteropServices;
using System.Diagnostics;
namespace Lanceur
{
        /// 
       /// Description résumée de CreateProcessWithLogon.
       /// 
      public class CreateProcessWithLogon
     {
               public const UInt32 Infinite = 0xffffffff;
              public const Int32 Startf_UseStdHandles = 0x00000100;
              public const Int32 StdOutputHandle = -11;
             public const Int32 StdErrorHandle = -12;
           [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
           public struct StartupInfo
  {
                public int cb;
               public String reserved;
               public String desktop;
              public String title;
              public int x;
              public int y;
              public int xSize;
              public int ySize;
              public int xCountChars;
              public int yCountChars;
              public int fillAttribute;
              public int flags;
              public UInt16 showWindow;
              public UInt16 reserved2;
              public byte reserved3;
              public IntPtr stdInput;
              public IntPtr stdOutput;
              public IntPtr stdError;

public struct ProcessInformation
{
                public IntPtr process;
               public IntPtr thread;
               public int processId;
               public int threadId;
}
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
public static extern bool CreateProcessWithLogonW(
String userName,
String domain,
String password,
UInt32 logonFlags,
String applicationName,
String commandLine,
UInt32 creationFlags,
UInt32 environment,
String currentDirectory,
ref StartupInfo startupInfo,
out ProcessInformation processInformation);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool GetExitCodeProcess(IntPtr process, ref UInt32 exitCode);
[DllImport("Kernel32.dll", SetLastError=true)]
public static extern UInt32 WaitForSingleObject(IntPtr handle, UInt32 milliseconds);
[DllImport("Kernel32.dll", SetLastError=true)]
public static extern IntPtr GetStdHandle(IntPtr handle);
[DllImport("Kernel32.dll", SetLastError=true)]
public static extern bool CloseHandle(IntPtr handle);
public static void Launch(string command, string user, string domain, string pwd){
StartupInfo startupInfo = new StartupInfo();
startupInfo.reserved = null;
startupInfo.flags &amp;= Startf_UseStdHandles;
startupInfo.stdOutput = (IntPtr)StdOutputHandle;
startupInfo.stdError = (IntPtr)StdErrorHandle;
UInt32 exitCode = 123456;
ProcessInformation processInfo = new ProcessInformation();
String currentDirectory = System.IO.Directory.GetCurrentDirectory();
try
{
CreateProcessWithLogonW(
user,
domain,
pwd,
(UInt32) 1,
command,
command,
(UInt32) 0,
(UInt32) 0,
currentDirectory,
ref startupInfo,
out processInfo);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Console.WriteLine("Exécution ...");
WaitForSingleObject(processInfo.process, Infinite);
GetExitCodeProcess(processInfo.process, ref exitCode);
Console.WriteLine("Code sortie: {0}", exitCode);
CloseHandle(processInfo.process);
CloseHandle(processInfo.thread);
}
}
}

 3. Dans le point d'entrée de l'application, j'ai ajouté le code pour vérifier l'existence du Framework .NET 3.5 sur le poste client en vérifiant la présence de clé dans la base de registre.
using System.Windows.Forms;
using System.Data;
using System.Configuration;
using System.Collections;
using Microsoft.Win32;
namespace Lanceur
{
///
/// Classe lanceur
///
public class MainClass
{
///
/// Point d'entrée principal de l'application.
///[STAThread]
static void Main()
{
string application = ConfigurationSettings.AppSettings["application"];
try
{
Console.WriteLine("Vérification de composant requis.");
// Vérifier si framework .NET3.5 est installé
if(!NETInstalled())
{
Console.WriteLine("Installation de composant requis en cours ....");
string cmd = @ConfigurationSettings.AppSettings["cmd"];

// Installation en silence
cmd = cmd + " /q:a /c:'setup.exe /q /norestart' /norestart";
string user = ConfigurationSettings.AppSettings["user"];
string domain = ConfigurationSettings.AppSettings["domain"];
string pwd = ConfigurationSettings.AppSettings["pwd"]; 

// Installation du framework .NET 3.5
CreateProcessWithLogon.Launch(cmd, user,domain,pwd);
Console.WriteLine("Installation de composant requis est teminé.");

// Installation de l'application
Console.WriteLine("Installation de l'application.");
InstallApplication(application);
}
else
{
// Installation de l'application
Console.WriteLine("Installation de l'application.");
InstallApplication(application);
}
}
catch(Exception Ex)
{…}

// Lancement de l'application
try
{
// Code propre à l'application
Application.Run((System.Windows.Forms.Form)(IHMManager.Instance.getIhmPrincipale()));
}
catch(….)
{}
}
}
private static bool NETInstalled()
{
RegistryKey cle = Registry.LocalMachine.OpenSubKey("Software\\Microsoft\\NET Framework Setup\\NDP\\v3.5");
         if(cle == null)
        {
return false;
        }
       return true;
}

private static void InstallApplication(string application)
{
    try
  {
       // Prévenir l'utilisateur qi tout se passe bien, de passer par la nouvelle icône pour la prochaine connexion        MessageBox.Show("L'application [NOMAPPLICATION] sera installée sur votre poste. Merci de cliquer sur  la nouvelle icône sur votre bureau pour la prochaine connexion.");
System.Threading.Thread.Sleep(500);
System.Diagnostics.Process.Start(@application);
System.Diagnostics.Process.Start(@application);
 }
catch(Exception Ex)
{
throw Ex;
}
}
}
}

Cette solution n'a finalement pas été retenue. Mais cela me permet de découvrir une possibilité sur le moyen d'installation les éléments requis. 

On utilise finalement un outil PsTools->PsExec qui permet d'installer à distance et en masse une application sur un poste client sous contexte d'un administrateur.

jeudi 8 juillet 2010

Astuce pour afficher les résultats de requête sur une seule ligne/sous forme d’une chaine de caractères

Une fois je voulais récupérer les données d’une table de la base de données SQL Server 2008 sous forme d’une chaîne de caractères. Je pensais faire une fonction avec un curseur et concatène des données de chaque ligne pour former une chaîne de caractères.

Supposons, il y a une table suivante dans une base de données :
Table dbo.PAYS
ID LIBELLE
1 France
2 Allemagne
3 Belgique
4 Espagne

Lorsque l’on fait la requête: SELECT LIBELLE FROM dbo.PAYS, le résultat affiché:
LIBELLE
France
Allemagne
Belgique
Espagne

Une astuce facile pour former une chaîne de caractère à partir de ce résultat est d’utiliser le mot clé : FOR XML PATH

Le mode FOR XML PATH est utilisé pour construire un XML à partir de résultats de requête.

Examples:
Requête
SELECT dbo.PAYS.LIBELLE
FROM dbo.PAYS
ORDER BY dbo.PAYS.ID
For XML PATH
Résultat
<row>
<LIBELLE>France</LIBELLE>
</row>
<row>
<LIBELLE>Allemagne</LIBELLE>
</row>
<row>
<LIBELLE>Belgique</LIBELLE>
<row>
<row>
<LIBELLE>Espagne</LIBELLE>
</row>

Requête:
SELECT dbo.PAYS.LIBELLE + ''
FROM dbo.PAYS
ORDER BY dbo.PAYS.ID
For XML PATH ('Pays')
Résultat :
<Pays>France</Pays>
<Pays>Allemagne</Pays>
<Pays>Belgique</Pays>
<Pays>Espagne</Pays>

Et, la requête suivante:
SELECT dbo.PAYS.LIBELLE + ','
FROM dbo.PAYS
ORDER BY dbo.PAYS.ID
For XML PATH ('')
Et voila le résultat:
France,Allemagne,Belgique,Espagne

Voir plus loin sur FOR XML PATH :
http://technet.microsoft.com/fr-fr/library/ms189885(SQL.90).aspx

mercredi 7 juillet 2010

Problème de génération Word via une tâche planifiée

J’ai travaillé sur un projet .NET C# qui génère des documents Word via un exécutable lancé par une tâche planifiée. J'utilise Interop.Word pour la génération des documents.

L'exécutable a été installé sur Windows Server 2003, la génération se passait bien.
Récemment, on a migré l'application sur Windows Server 2008. Depuis la génération ne marchait plus.

Apres plusieurs recherches, j'ai pu trouver le "hack" pour contourner ce problème:

1. Sur Windows x86
-Créer le répertoire c:\\windows\system32\config\systemprofile\desktop
-Accorder les droits au compte qui exécute la génération Word

2. Sur Windows x64
-Créer le répertoire c:\\windows\SysWOW64\config\systemprofile\desktop
-Ajouter les droits au compte qui exécute la génération Word

Le répertoire Desktop est apparemment nécessaire pour l’ouverture du fichier Office. Ce répertoire existe sur Windows Server 2003 mais disparaît sur Windows Server 2008.

Pour suivre la discussion sur ce sujet:
http://social.msdn.microsoft.com/Forums/en/innovateonoffice/thread/b81a3c4e-62db-488b-af06-44421818ef91