Bueno más o menos estoy de acuerdo con
BasicOS, ya sabéis mis opiniones sobre la preferencia sobre los INIs, incluso con portables. (En realidad este hilo empezó en el que enlaza
Chefito al principio, esas cosas se discuten allí).
Pero también estoy de acuerdo en que quizás en algunas ocasiones una técnica así sea interesante. Y desde luego como ejercicio de programación ha sido muy interesante.
He aquí mi versión, la verdad es que me gusta como ha quedado, porque
¡¡al final ni FileInstall, ni EXEs de apoyo, ni archivos BAT!!
El problema de no poder (o no saber) modificar el EXE desde el bat (sí añadir, pero no cambiar) lo soluciono lógicamente haciéndolo desde el EXE. Casi todo lo hago desde el EXE, y el BAT queda tan simple... que ni uso un archivo, mando el comando con un
Run(@ComSpec & ' /c'...) y como eso se ejecuta asíncronamente puedo cerrar el EXE para que ese comando acabe el trabajo. La estructura de trabajo es la siguiente:
EN EL EXE:
.- Al iniciar buscar datos, si hay los toma y si no da valores por defecto (es decir, lo he hecho para que no sea necesario que el EXE contenga datos, ideal para no tener que manipular una primera compilación)
.- Interacción con el usuario.
.- Al finalizar, si el usuario quiere guardar cambios:
----Si los datos no han cambiado simplemente salimos
----Si han cambiado hacemos copia del EXE (que no es de escritura pero sí de lectura).
----Cambiamos datos EN LA COPIA del EXE
----Mandamos comando CMD de finalización y acabamos el programa
En el comando CMD de finalización:
.- Espera unos segundos a que se cierre el exe
.- Sustituye el EXE original por el modificado con un MOVE
Sobre los detalles de implementación de cómo hacer estas cosas podéis ver el código comentado al final de este comentario. Comento aquí algunas generalidades:
En el ejemplo sólo guardo como dato un String,
¡pero para nada hace esto un ejemplo trivial!. Pensad que ese String puede ser un montón de datos a extraer con un simple StringSplit o el texto de un archivo INI.
Por supuesto aún se puede ir más allá y guardar datos binarios (como imágenes), de hecho la cadena la tengo que almacenar en binario... Para manejar datos binarios lo mejor supongo que sería montarse estructuras con las funciones
DllStruct....
Trabajo en binario casi todo el rato porque el EXE es binario, claro, pero me he encontrado con pocas funciones para trabajar en binario puro así que al final manejo representaciones de ese binario en String para poder hacer búsquedas, extraer fragmentos, añadir... (sí existe por ejemplo
BinaryMid, pero falta lo demás). También es cierto que estoy trabajando con la versión 3.3 de AutoIt, igual me estoy perdiendo algo pero no recuerdo haber visto ampliación de funciones para binario en los "History".
Para esperarme en el comando de consola a que cierre el EXE no he encontrado un método plenamente satisfactorio. Al principio hacía un
DIR de Windows\system32 para que se tirara un rato haciendo algo, pero eso es muy variable

. Buscando comandos estándar que me dieran una pausa controlable sólo he encontrado
ping, con el parámetro
-w se podría hacer esperar el tiempo que queramos con una IP bloqueante, pero eso es peliagudo, además no es aplicable si no hay acceso internet... Al menos se puede usar el hecho de que cuando haces varios pings (parámetro -n) entre ping y ping se espera algo así como un segundo. Se puede entonces hacer ping al bucle local (127.0.0.1) que tiene la gran ventaja que existe aunque no haya internet conectado y que no da otras demoras pues responde enseguida.
Actualmente le doy 3 segundos antes de hacer el MOVE, aunque he visto que es tiempo de sobra.
Así que no ejecutéis enseguida después de salir o no veréis cambios, ¡esperad 3 segundos a que tenga tiempo de poner el nuevo EXE!
Posibles mejoras:
.- Comprobar si el EXE es modificable (no está en un CD, etc.) y si no lo es advertir al usuario de que no podrá guardar datos.
.- Si hay mecanismos para trabajar directamente en binario, que quizás existan pero yo simplemente desconozco, pues usarlos para que sea más eficiente que estar pasando a String (que además ocupa más, pues cada byte se expande a dos)
.- Si se va a tratar con EXEs y datos grandes quizás sea mejor leer el EXE a fragmentos descartando hasta que se encuentre la marca de datos, y tomando a partir de ahí. También se pueden ir leyendo fragmentos desde el final (posicionándose con FileGetPos) pues los datos están siempre al final. Si son de tamaño fijo incluso se puede tomar el tamaño concreto y no hace falta ni marca de datos.
.- Con Exes/Datos grandes mejor trabajar en la misma unidad de disco, así el MOVE no tiene que copiar si no está en la unidad del sistema (para ser "aseado" trabajo en el temporal de Windows). Eso tiene el inconveniente de que el usuario ve aparecer el archivo temporal, pero si son varias megas...
De todas maneras para varias megas yo no usaría este tipo de técnicas de datos en EXE, para eso mejor un INI o incluso una base de datos...
Ala, espero que os guste, mejor compilarlo con las opciones por defecto (para que comprima con UPX y así ocupe menos y sea todo más rápido). La primera vez no tiene datos, no pasa nada, como digo el programa lo comprueba y ofrece un valor por defecto (que no guardará si no se cambia).
Código: Seleccionar todo
#cs ----------------------------------------------------------------------------
Script: GuardaDatosEnEXE.au3
Versión AutoIt: 3.3.0.0
Autor: Ximo Izquierdo
Objetivo del Script:
Implementa mecanismo de guardado de datos en el propio EXE
#ce ----------------------------------------------------------------------------
#include <GUIConstantsEx.au3>
Opt("MustDeclareVars",1)
If Not @Compiled Then
MsgBox(48, "Ejecución Abortada", "Este script sólo puede ejecutarse si está compilado.")
Exit
EndIf
; Marca de inicio de datos en el EXE, como mis datos son texto puede ser muy simple:
; algunos caracteres no imprimibles más algo de texto para asegurarme.
; La condición es que la marca no pueda estar contenida en los datos.
; Podría ser aún más simple si obligo a que el EXE siempre lleve datos, cosa que no hago,
; pero si no ojo por si esa combinación se da en el binario original...
; (si siempre hay datos no es problema porque como busco desde el final la primera ocurrencia
; que encuentre será mi marca)
; Lo paso a binario y me quedo con su representación en string quitando el "0x" inicial
Global $marcaInicioDatos = _BytesString(Chr(0)&Chr(1)&"DATA")
Global $DATO, $txtDato, $btnCancelar, $btnGuardar
GuiCreate("Ejecutable con datos incrustados guardables", 370,160)
$DATO = _LeeDatoGuardado()
GUICtrlCreateLabel("Dato actualmente guardado: " & @LF & $DATO, 25,16, 300,30)
GUICtrlSetFont(-1, 9.5, 800)
GUICtrlCreateLabel("Escriba cadena a guardar:", 25,64, 129,17)
$txtDato = GUICtrlCreateInput($DATO, 25,83, 320,21)
$btnCancelar = GUICtrlCreateButton("Cancelar", 72,123, 100,24)
$btnGuardar = GUICtrlCreateButton("Guardar y Salir", 192,123, 100,24 )
GUICtrlSetState(-1, $GUI_DEFBUTTON)
GUISetState(@SW_SHOW)
While 1
Switch GUIGetMsg()
Case $btnGuardar
_SalirGuardando(GUICtrlRead($txtDato))
Case $GUI_EVENT_CLOSE, $btnCancelar
ExitLoop
EndSwitch
WEnd
Func _LeeDatoGuardado()
Local $fExe, $fBin, $pos
;Buscamos marca de datos en el ejecutable
$fExe = FileOpen(@ScriptFullPath, 16)
$fBin = String(FileRead($fExe))
FileClose($fExe)
$pos = StringInStr($fBin, $marcaInicioDatos, 2, -1)
If $pos <> 0 Then
; Marca encontrada, extraemos los datos, están desde después de la marca hasta el final
Return BinaryToString("0x" & StringMid($fBin, $pos+StringLen($marcaInicioDatos)))
Else
; No hay datos, devolvemos valor por defecto
Return "¡Autoit es una pasada!"
EndIf
EndFunc
Func _SalirGuardando($txt)
Local $datoBin, $fExe, $fBin, $nomTemp, $fTemp, $pos, $cmd
If $DATO == $txt Then Exit ; Dato no cambiado, salimos sin más
; Leemos EXE y buscamos inicio de datos si existe
$fExe = FileOpen(@ScriptFullPath, 16)
$fBin = String(FileRead($fExe))
FileClose($fExe)
$pos = StringInStr($fBin, $marcaInicioDatos, 2, -1) ;buscamos desde el final
; Si ya había datos los quitamos antes de poner los nuevos
If $pos <> 0 Then $fBin = StringMid($fBin, 1, $pos-1)
;Metemos nuevo dato y creamos el EXE modificado
$fBin &= $marcaInicioDatos & _BytesString($txt)
$nomTemp = @TempDir & "\" & @ScriptName & ".TEMP"
$fTemp = FileOpen($nomTemp, 18) ;Escritura binaria, nuevo archivo
FileWrite($fTemp, Binary($fBin))
FileClose($fTemp)
; Mandamos a la consola el comando que sustituye el EXE en ejecución por el modificado y salimos
; Esperamos 3 segundos antes de mover el archivo, debería ser tiempo de sobra para desbloquear el ejecutable
; Nombres de archivo entre comillas por si llevan espacios
$cmd = @ComSpec & ' /c ping 127.0.0.1 -n 3 > nul && MOVE /Y "' & $nomTemp & '" "' & @ScriptFullPath & '"'
;ConsoleWrite($cmd & @LF)
Run($cmd, @TempDir, @SW_HIDE)
Exit
EndFunc
Func _BytesString($expr)
;Devuelve en un String la representación binaria de una expresión, sin el "0x" inicial
Return StringTrimLeft(Binary($expr), 2)
EndFunc