NovaMonoFix
Errores PHP
X
Usuario
Password
0 FPS

Tutorial WinAPI C++ 3.10 (Terminando el Instalador)

06 de Noviembre del 2010 por Josep Antoni Bover, 26 visitas, 0 comentarios, 0 votos
Categorías : Windows, Programación, C y C++.
La web está en segundo plano, animación en pausa.
Cargando animación...
Tutorial WinAPI C++ 3.10 (Terminando el Instalador)

En este tutorial vamos a empezar a jutar las piezas para formar nuestro instalador. En primer lugar usaremos los controles que hemos creado desde cero para el interface grafico del instalador, y luego le añadiremos el ObjetoZLIB para poder descomprimir datos.

Tambien usaremos una parte de los common dialogs que tocamos antes, en concreto vamos a usar el ObjetoDlgDirectorios que nos servira para seleccionar el directorio destino de la instalacion.

Hay que remarcar que este punto es algo complicado dependiendo de las capacidades deseadas. Para empezar todo ejecutable creado con VC requiere unas dlls mínimas para funcionar, si el sistema operativo no tiene esas dlls, el ejecutable no arrancara.

Esto es un problema, ya que si compilamos el instalador con VisualStudio 2010 este requerirá msvcp100.dll y msvcr100.dll. Esas dlls no vienen por defecto en ningún sistema operativo hasta la fecha (Puede que en instalaciones de windows 7 SP1 ya vengan dichas dlls, aunque aun no he tenido oportunidad de comprobarlo, pero en windows 7 a pelo sin service pack no vienen) por lo que si queremos que el instalador se ejecute, esas dlls deberán estar en el mismo directorio que se vaya a ejecutar el instalador.. Con lo que al final la única solución es distribuir un zip con el instalar.exe y las 2 dlls dentro.

Si deseamos que el Instalar.exe funcione solo sin necesitar runtimes extras, necesitaremos compilar el instalar.exe con algún runtime que sepamos que ya está instalado en la maquina destino. Por ejemplo si queremos hacer un instalador para windows 2000/ME o superior… lo ideal sería hacerlo en VC6.

Por supuesto estoy hablando de casos extremos en los que no deseamos utilizar el instalador de visual studio y nos queremos hacer uno nosotros por lo que sea. Por ejemplo el instalador de BubaTronik que tiene un estilo como el reproductor, se ha hecho con VC6 para mantener compatibilidad con sistemas operativos antiguos.

Dicho esto ya podemos empezar a ver código, lo mejor será empezar por el principio así que echaremos un vistazo al WinMain :

Archivo : Tutorial TerminandoInstalador.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// WinMain
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow) {
// Comprobamos si el usuario es administrador
// NO ES ADMINISTRADOR
if (IsUserAnAdmin() == FALSE) {
int TotalArgs = 0;
TCHAR **Args = CommandLineToArgvW(GetCommandLine(), &TotalArgs);
// Ejecutamos el instalador de forma que pida privilegios de administrador
ShellExecute(NULL, TEXT("RunAs"), Args[0], NULL, NULL, SW_SHOWNORMAL);
LocalFree(Args);
}
// ES ADMINISTRADOR
else {
// El programa se ha arrancado con privilegios de administrador,
// mostramos la ventana de instalación
VentanaInstalador Ventana;
MSG Mensaje;
while (TRUE == GetMessage(&Mensaje, NULL, 0, 0)) {
TranslateMessage(&Mensaje);
DispatchMessage(&Mensaje);
}
}
return 0;
}

Lo primero que hacemos es utilizar la API IsUserAnAdmin para determinar si el programa se esta ejecutando con privilegios de administrador.

- En el caso de no ser administrador utilizaremos las API's CommandLineToArgvW y GetCommandLine para obtener la ruta completa de nuestro ejecutable, con esa ruta ejecutaremos nuestro ejecutable de nuevo de forma que pida privilegios de administrador (utilizando la API ShellExecute especificando en el parametro lpOperation "RunAs"). Al especificar RunAs en el parametro lpOperation le estamos diciendo al Windows que nuestra aplicacion necesita privilegios de administrador.

Luego borraremos la memoria que crea CommandLineToArgvW con la API LocalFree.

- En el caso de ser administrador continuaremos la ejecución normal mostrando la ventana del instalador.

En windows xp por defecto todas las aplicaciones se ejecutan como administrador. En Windows Vista y superiores las aplicaciones se ejecutan como usuario sin privilegios por defecto, y al estar sin permisos de escritura en la mayoría del disco, el instalador no podría crear los archivos descomprimidos ni los directorios (tampoco podría escribir en ciertas partes del registro de windows).
La API IsUserAnAdmin no existe en VC6, además los programas que usen el runtime del VC6 son lanzados automáticamente pidiendo permisos de administrador.

Bueno con esto ya hemos visto como podemos hacer que nuestra aplicación pida privilegios de administrador, ahora pasemos a ver el objeto VentanaInstalador :

Archivo : VentanaInstalador.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Objeto final : ObjetoHWND -> PlantillaEventos -> ObjetoVentana -> VentanaInstalador
class VentanaInstalador : public ObjetoVentana {
public : ///////////////// Miembros publicos
// - Constructor
VentanaInstalador(void);
protected : ////////////// Miembros portegidos
// - Función que crea la ventana del instalador
void Crear(void);
// - Función que determina si el instalador está lleno o vacio, y en el caso de estar lleno
// lee los valores de la instalación y los asigna a sus correspondientes controles.
BOOL LeerDatos(void);
// - Función que enlaza con el mensaje WM_CLOSE
LRESULT Evento_Cerrar(void);
// - Función que enlaza con el mensaje WM_PAINT
LRESULT Evento_Pintar(HDC hDC, PAINTSTRUCT &PS);
// - Función que recibe cuando se hace click en un ObjetoBoton
LRESULT Evento_ObjetoBoton_Click(ObjetoBoton *BotonPresionado, const UINT nBoton);
// - Función para mostrar al usuario un dialogo para seleccionar un directorio
void SeleccionarDirectorio(void);
// - Función que valida los datos y empieza la instalacion
void InstalarContenido(void);
// - Barra de progreso
ObjetoBarraProgreso Barra;
// - EditBox que indica la ruta destino
ObjetoEditBox PathDestino;
// - Botón para instalar
ObjetoBoton Instalar;
// - Botón para salir
ObjetoBoton Salir;
// - Botón para buscar el directorio
ObjetoBoton BuscarDirectorio;
// - Archivo que contiene toda la instalacion
ObjetoArchivo InstalarExe;
// - Función que comprueba si la ruta especificada es valida
BOOL _PathValido(const TCHAR *nText);
// - Función para mostrar el ultimo error con FormatMessage añadiendo un texto personalizado
void _MostrarUltimoError(const TCHAR *Mensaje);
};

No he dejado ningún miembro público de forma que cuando se inicie el constructor se cree directamente la ventana y los controles necesarios.
Las funciones mas interesantes por ver son : LeerDatos, InstalarContenido y _MostrarUltimoError. Las demás funciones mas o menos se han tratado con anterioridad.

Empezemos por la función LeerDatos, esta función se encargara de determinar si hay datos dentro del instalar.exe o es un instalador vacio, y en el caso de haber datos extraerá los primeros que necesitamos.

Archivo : VentanaInstalador.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// - Función que determina si el instalador está lleno o vacio, y en el caso de estar lleno
// lee los valores de la instalación y los asigna a sus correspondientes controles.
BOOL VentanaInstalador::LeerDatos(void) {
LPWSTR Path = GetCommandLine();
TCHAR PathInstalarExe[MAX_PATH + 1];
// 1 - Parseamos el path por si viene con comillas
UINT TamPath = wcslen(Path) + 1;
UINT Contador = 0;
for (UINT i = 0; i < TamPath; i++) {
if (Path[i] != TEXT('"')) {
PathInstalarExe[Contador] = Path[i];
Contador ++;
}
}
// 2 - Abrimos el instalador para lectura
if (InstalarExe.AbrirArchivoLectura(PathInstalarExe) == FALSE) {
_MostrarUltimoError(TEXT("Error abriendo el instalador..."));
return FALSE;
}
// 3 - Leemos los datos del instalar.exe
TCHAR Tmp[512];
// Nos desplazamos al final del archivo menos ((sizeof(TCHAR) * 32) + sizeof(UINT)
// La cabecera son 32 caracteres del tipo TCHAR y ademas le sumamos el tamaño de un UINT.
// El UINT será el tamaño del ejecutable limpio
UINT TamInstalarExeReal = 0;
InstalarExe.Posicion(-static_cast<long>(((sizeof(TCHAR) * 32) + sizeof(UINT))), true);
InstalarExe.LeerUINT(TamInstalarExeReal);
// - Leemos la cabecera para determinar si el instalador esta lleno o vacio
InstalarExe.Leer(Tmp, sizeof(TCHAR) * 32);
TCHAR Cabecera[32] = TEXT("Instalador 1.0 devildrey33 ");
if (wcscmp(Tmp, Cabecera) != 0) {
// Instalador vacio
Instalar.Activado(false);
TamInstalarExeReal = 0;
return FALSE;
}
// 4 - Nos movemos a la posición donde termina el instalar.exe y empiezan los datos
InstalarExe.Posicion(TamInstalarExeReal, false);
// 5 - Obtenemos el Path por defecto de la instalación
UINT NumPathDefecto = 0;
InstalarExe.LeerUINT(NumPathDefecto);
TCHAR *PathDefecto = NULL;
TCHAR PathDefecto2[MAX_PATH];
InstalarExe.LeerString(PathDefecto);
ObjetoDirectoriosWindows Directorios;
TCHAR PathDestinoInicial[MAX_PATH];
wcscpy_s(PathDestinoInicial, MAX_PATH, PathDefecto);
switch (NumPathDefecto) {
case 0 : // C:
wcscpy_s(PathDefecto2, 4, TEXT("C:\\"));
break;
case 1 : // Archivos de programa x86
Directorios.ArchivosDeProgramaX86(PathDefecto2);
break;
case 2 : // Archivos de programa x64
// Directorios.ArchivosDeProgramaX64(PathDefecto);
break;
}
// 6 - Asignamos el path por defecto de la instalación al editbox
swprintf_s(PathDestinoInicial, TEXT("%s\\%s"), PathDefecto2, PathDefecto);
PathDestino.AsignarTexto(PathDestinoInicial);
delete [] PathDefecto;
return TRUE;
}

Ahora veamos la función InstalarContenido que será la que se ejecutara al pulsar el botón instalar :

Archivo : VentanaInstalador.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
void VentanaInstalador::InstalarContenido(void) {
// 1 - Comprobamos que el directorio de instalacion sea valido
TCHAR InstalarDir[MAX_PATH];
TCHAR TmpSubDirectorio[MAX_PATH];
TCHAR TmpTxt[512];
PathDestino.ObtenerTexto(InstalarDir, MAX_PATH);
if (_PathValido(InstalarDir) == FALSE) {
MessageBox(_hWnd, TEXT("La ruta especificada no es válida.\n\
La ruta no puede tener los siguientes caracteres / : * ? \" < > |"),
TEXT("Error"), MB_OK);
return;
}
// 2 - Si el directorio tiene una antibarra al final la quitamos
UINT TamTmp = wcslen(InstalarDir);
if (InstalarDir[TamTmp] == TEXT('\\')) {
InstalarDir[TamTmp] = TEXT('\0');
}
// 3 - Creo el directorio de la instalación
BOOL RC = CreateDirectory(InstalarDir, NULL);
if (RC == FALSE && GetLastError() != 183) { // Error 183 "Error el directorio ya existe y no puede ser creado"
swprintf_s(TmpTxt, 512, TEXT("Error creando el directorio '%s' (%d). Instalacion abortada.\n"), InstalarDir, GetLastError());
_MostrarUltimoError(TmpTxt);
return;
}
// 4 - Leemos y creamos los subdirectorios
UINT TotalDirectorios = 0;
UINT i = 0;
PTCHAR TmpDir;
InstalarExe.LeerUINT(TotalDirectorios);
for (i = 0; i < TotalDirectorios; i++) {
InstalarExe.LeerString(TmpDir);
swprintf_s(TmpSubDirectorio, MAX_PATH, TEXT("%s%s"), InstalarDir, TmpDir);
MessageBox(_hWnd, TmpSubDirectorio, TEXT("a"), MB_OK);
delete [] TmpDir;
RC = CreateDirectory(TmpSubDirectorio, NULL);
if (RC == FALSE && GetLastError() != 183) { // Error 183 "Error el directorio ya existe y no puede ser creado"
swprintf_s( TmpTxt, 512, TEXT("Error creando el directorio : '%s' (%d). Instalacion abortada.\n"),
TmpSubDirectorio, GetLastError());
_MostrarUltimoError(TmpTxt);
return;
}
}
// 5 - Descomprimimos y creamos los archivos finales
UINT TotalArchivos = 0;
UINT TamArchivo = 0;
MSG msg;
char *Datos = NULL;
ObjetoZLIB ZLIB;
ContenedorBinario ArchivoComprimido;
InstalarExe.LeerUINT(TotalArchivos);
Barra.Maximo(static_cast<float>(TotalArchivos));
for (i = 0; i < TotalArchivos; i++) {
InstalarExe.LeerString(TmpDir);
swprintf_s(TmpSubDirectorio, MAX_PATH, TEXT("%s%s"), InstalarDir, TmpDir);
delete [] TmpDir;
InstalarExe.LeerUINT(TamArchivo);
Datos = new char[TamArchivo];
InstalarExe.Leer(Datos, TamArchivo * sizeof(char));
ArchivoComprimido.Borrar();
ArchivoComprimido.AgregarParte(Datos, TamArchivo);
if (ZLIB.Descomprimir(ArchivoComprimido, TmpSubDirectorio) == FALSE) {
// error creando o descomprimiendo archivo
swprintf_s(TmpTxt, 512, TEXT("Error creando el archivo : '%s'.\n"), TmpSubDirectorio);
MessageBox(_hWnd, TmpTxt, TEXT("Error"), MB_OK);
}
Barra.Valor(static_cast<float>(i));
// esperar eventos
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
MessageBox(NULL, TEXT("Instalación completada!"), TEXT("Instalación completada"), MB_OK);
}
Tabla que nos muestra un instalador por dentro :
Instalar.exe (BINARO) : Esta parte contiene el ejecutable que descomprimirá los datos.
Directorio destino : UINT con el directorio por defecto, String con el nombre del directorio.
Subdirectorios a crear : UINT con el total de directorios, String * total directorios.
Archivos comprimidos : UINT con el total de archivos, String con el path, UINT con el tamaño de los datos binarios, char * tamaño.
Cabecera : DWORD con la posición de inicio del instalador, TCHAR * 32 con este texto “Instalador 1.0 devildrey33     “
un String se compone de : UINT con el tamaño en caracteres, TCHAR * tamaño en caracteres.

Ya solo nos falta ver la función _MostrarUltimoError :

Archivo : VentanaInstalador.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Función para mostrar el ultimo error con FormatMessage añadiendo un texto personalizado
void VentanaInstalador::_MostrarUltimoError(const TCHAR *Mensaje) {
DWORD ErrNum = GetLastError();
LPVOID lpMsgBuf;
if (ErrNum == 0) return;
FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, ErrNum, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL );
if (Mensaje == NULL) {
MessageBox(NULL, (LPCTSTR)lpMsgBuf, TEXT("Error!"), MB_OK | MB_ICONINFORMATION);
}
else {
TCHAR Buffer[2048];
wsprintf(Buffer, TEXT("%s\n%s"), Mensaje, (LPCTSTR)lpMsgBuf);
MessageBox(NULL, Buffer, TEXT("Error"), MB_OK | MB_ICONINFORMATION);
}
LocalFree(lpMsgBuf);
}

Esta función tiene por objetivo mostrar un mensaje de error con la información que nos da la API GetLastError. Con el número del error podemos utilizar FormatMessage para obtener un mensaje de error en formato de texto, que es bastante mas descriptivo que un numero. Por último mostramos el error con la API MessageBox.

Y con esto hemos terminado con el instalador. Ya solo nos queda el último paso : 3.11 Tutorial terminando el Ensamblador.