mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-22 23:11:00 -04:00
595 lines
17 KiB
C++
595 lines
17 KiB
C++
#ifndef UNICODE
|
|
#define UNICODE
|
|
#endif
|
|
|
|
#ifndef _UNICODE
|
|
#define _UNICODE
|
|
#endif
|
|
|
|
#include <Windows.h>
|
|
#include <Shlobj.h>
|
|
#include <Shlwapi.h>
|
|
#include <Shellapi.h>
|
|
#include <Psapi.h>
|
|
#include <wchar.h>
|
|
#include <stdio.h>
|
|
#include <io.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include <easylzma/decompress.h>
|
|
#include "XUnzip.h"
|
|
|
|
#define BUFSIZE 4096
|
|
|
|
// Error handling {{{
|
|
|
|
static void show_error(LPCWSTR msg) {
|
|
MessageBeep(MB_ICONERROR);
|
|
MessageBox(NULL, msg, L"Error", MB_OK|MB_ICONERROR);
|
|
}
|
|
|
|
static void show_detailed_error(LPCWSTR preamble, LPCWSTR msg, int code) {
|
|
LPWSTR buf;
|
|
buf = (LPWSTR)LocalAlloc(LMEM_ZEROINIT, sizeof(WCHAR)*
|
|
(wcslen(msg) + wcslen(preamble) + 80));
|
|
|
|
_snwprintf_s(buf,
|
|
LocalSize(buf) / sizeof(WCHAR), _TRUNCATE,
|
|
L"%s\r\n %s (Error Code: %d)\r\n",
|
|
preamble, msg, code);
|
|
|
|
show_error(buf);
|
|
LocalFree(buf);
|
|
}
|
|
|
|
static void show_zip_error(LPCWSTR preamble, LPCWSTR msg, ZRESULT code) {
|
|
LPWSTR buf;
|
|
char msgbuf[1024] = {0};
|
|
|
|
FormatZipMessage(code, msgbuf, 1024);
|
|
|
|
buf = (LPWSTR)LocalAlloc(LMEM_ZEROINIT, sizeof(WCHAR)*
|
|
(wcslen(preamble) + wcslen(msg) + 1100));
|
|
|
|
_snwprintf_s(buf,
|
|
LocalSize(buf) / sizeof(WCHAR), _TRUNCATE,
|
|
L"%s\r\n %s (Error: %S)\r\n",
|
|
preamble, msg, msgbuf);
|
|
|
|
show_error(buf);
|
|
LocalFree(buf);
|
|
}
|
|
|
|
static void show_last_error_crt(LPCWSTR preamble) {
|
|
WCHAR buf[BUFSIZE];
|
|
int err = 0;
|
|
|
|
_get_errno(&err);
|
|
_wcserror_s(buf, BUFSIZE, err);
|
|
show_detailed_error(preamble, buf, err);
|
|
}
|
|
|
|
static void show_last_error(LPCWSTR preamble) {
|
|
WCHAR *msg = NULL;
|
|
DWORD dw = GetLastError();
|
|
|
|
FormatMessage(
|
|
FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
|
FORMAT_MESSAGE_FROM_SYSTEM |
|
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
NULL,
|
|
dw,
|
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
(LPWSTR)&msg,
|
|
0, NULL );
|
|
|
|
show_detailed_error(preamble, msg, (int)dw);
|
|
}
|
|
|
|
// }}}
|
|
|
|
// Load, decompress and extract data {{{
|
|
|
|
static BOOL load_data(LPVOID *data, DWORD *sz) {
|
|
HRSRC rsrc;
|
|
HGLOBAL h;
|
|
|
|
rsrc = FindResourceW(NULL, L"extra", L"extra");
|
|
if (rsrc == NULL) { show_last_error(L"Failed to find portable data in exe"); return false; }
|
|
|
|
h = LoadResource(NULL, rsrc);
|
|
if (h == NULL) { show_last_error(L"Failed to load portable data from exe"); return false; }
|
|
|
|
*data = LockResource(h);
|
|
if (*data == NULL) { show_last_error(L"Failed to lock portable data in exe"); return false; }
|
|
|
|
*sz = SizeofResource(NULL, rsrc);
|
|
if (sz == 0) { show_last_error(L"Failed to get size of portable data in exe"); return false; }
|
|
|
|
return true;
|
|
}
|
|
|
|
static BOOL unzip(HZIP zipf, int nitems, IProgressDialog *pd) {
|
|
int i = 0;
|
|
ZRESULT res;
|
|
ZIPENTRYW ze;
|
|
|
|
for (i = 0; i < nitems; i++) {
|
|
res = GetZipItem(zipf, i, &ze);
|
|
if (res != ZR_OK) { show_zip_error(L"Failed to get zip item", L"", res); return false;}
|
|
|
|
res = UnzipItem(zipf, i, ze.name, 0, ZIP_FILENAME);
|
|
if (res != ZR_OK) { CloseZip(zipf); show_zip_error(L"Failed to extract zip item (is your disk full?):", ze.name, res); return false;}
|
|
|
|
pd->SetLine(2, ze.name, true, NULL);
|
|
pd->SetProgress(i, nitems);
|
|
}
|
|
|
|
CloseZip(zipf);
|
|
|
|
return true;
|
|
}
|
|
|
|
static HANDLE temp_file(LPWSTR name) {
|
|
UINT res;
|
|
HANDLE h;
|
|
|
|
res = GetTempFileNameW(L".", L"portable_data", 0, name);
|
|
|
|
if (res == 0) { show_last_error(L"Failed to create temporary file to decompress portable data"); return INVALID_HANDLE_VALUE; }
|
|
|
|
h = CreateFile(name, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
|
if (h == INVALID_HANDLE_VALUE) { show_last_error(L"Failed to open temp file to decompress portable data"); }
|
|
return h;
|
|
|
|
}
|
|
|
|
struct DataStream
|
|
{
|
|
const unsigned char *in_data;
|
|
size_t in_len;
|
|
|
|
HANDLE out;
|
|
IProgressDialog *pd;
|
|
};
|
|
|
|
static int
|
|
input_callback(void *ctx, void *buf, size_t * size)
|
|
{
|
|
size_t rd = 0;
|
|
struct DataStream * ds = (struct DataStream *) ctx;
|
|
|
|
rd = (ds->in_len < *size) ? ds->in_len : *size;
|
|
|
|
if (rd > 0) {
|
|
memcpy(buf, (void*) ds->in_data, rd);
|
|
ds->in_data += rd;
|
|
ds->in_len -= rd;
|
|
}
|
|
|
|
*size = rd;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int output_error_shown = 0;
|
|
|
|
static size_t
|
|
output_callback(void *ctx, const void *buf, size_t size)
|
|
{
|
|
struct DataStream * ds = (struct DataStream *) ctx;
|
|
DWORD written = 0;
|
|
|
|
if (size > 0) {
|
|
if (!WriteFile(ds->out, buf, size, &written, NULL)) {
|
|
show_last_error(L"Failed to write uncompressed data to temp file");
|
|
output_error_shown = 1;
|
|
return 0;
|
|
}
|
|
written = SetFilePointer(ds->out, 0, NULL, FILE_CURRENT);
|
|
ds->pd->SetProgress(written, UNCOMPRESSED_SIZE);
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
static BOOL decompress(LPVOID src, DWORD src_sz, HANDLE out, IProgressDialog *pd) {
|
|
elzma_decompress_handle h;
|
|
struct DataStream ds;
|
|
int rc;
|
|
|
|
h = elzma_decompress_alloc();
|
|
|
|
if (h == NULL) { show_error(L"Out of memory"); return false; }
|
|
|
|
ds.in_data = (unsigned char*)src;
|
|
ds.in_len = src_sz;
|
|
ds.out = out;
|
|
ds.pd = pd;
|
|
|
|
rc = elzma_decompress_run(h, input_callback, (void *) &ds, output_callback,
|
|
(void *) &ds, ELZMA_lzip);
|
|
|
|
if (rc != ELZMA_E_OK) {
|
|
if (!output_error_shown) show_detailed_error(L"Failed to decompress portable data", L"", rc);
|
|
elzma_decompress_free(&h);
|
|
return false;
|
|
}
|
|
|
|
elzma_decompress_free(&h);
|
|
|
|
return true;
|
|
}
|
|
|
|
static BOOL extract(LPVOID cdata, DWORD csz) {
|
|
HANDLE h;
|
|
WCHAR tempnam[MAX_PATH+1] = {0};
|
|
BOOL ret = true;
|
|
HZIP zipf;
|
|
ZIPENTRYW ze;
|
|
ZRESULT res;
|
|
int nitems;
|
|
HRESULT hr;
|
|
IProgressDialog *pd = NULL;
|
|
|
|
hr = CoCreateInstance(CLSID_ProgressDialog, NULL,
|
|
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pd));
|
|
|
|
if (FAILED(hr)) { show_error(L"Failed to create progress dialog"); return false; }
|
|
pd->SetTitle(L"Extracting Calibre Portable");
|
|
pd->SetLine(1, L"Decompressing data...", true, NULL);
|
|
|
|
h = temp_file(tempnam);
|
|
if (h == INVALID_HANDLE_VALUE) return false;
|
|
|
|
pd->StartProgressDialog(NULL, NULL, PROGDLG_NORMAL | PROGDLG_AUTOTIME | PROGDLG_NOCANCEL, NULL);
|
|
if (!decompress(cdata, csz, h, pd)) { ret = false; goto end; }
|
|
SetFilePointer(h, 0, NULL, FILE_BEGIN);
|
|
zipf = OpenZip(h, 0, ZIP_HANDLE);
|
|
if (zipf == 0) { show_last_error(L"Failed to open zipped portable data"); ret = false; goto end; }
|
|
|
|
res = GetZipItem(zipf, -1, &ze);
|
|
if (res != ZR_OK) { show_zip_error(L"Failed to get count of items in portable data", L"", res); ret = false; goto end;}
|
|
nitems = ze.index;
|
|
|
|
pd->SetLine(1, L"Copying files...", true, NULL);
|
|
if (!unzip(zipf, nitems, pd)) { ret = false; goto end; }
|
|
end:
|
|
pd->StopProgressDialog();
|
|
pd->Release();
|
|
CloseHandle(h);
|
|
DeleteFile(tempnam);
|
|
return ret;
|
|
}
|
|
|
|
// }}}
|
|
|
|
// Find calibre portable directory and install/upgrade into it {{{
|
|
|
|
static BOOL directory_exists( LPCWSTR path )
|
|
{
|
|
if( _waccess_s( path, 0 ) == 0 )
|
|
{
|
|
struct _stat status;
|
|
_wstat( path, &status );
|
|
return (status.st_mode & S_IFDIR) != 0;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static BOOL file_exists( LPCWSTR path )
|
|
{
|
|
if( _waccess_s( path, 0 ) == 0 )
|
|
{
|
|
struct _stat status;
|
|
_wstat( path, &status );
|
|
return (status.st_mode & S_IFREG) != 0;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static LPWSTR get_directory_from_user() {
|
|
WCHAR name[MAX_PATH+1] = {0};
|
|
LPWSTR path = NULL;
|
|
PIDLIST_ABSOLUTE ret;
|
|
|
|
path = (LPWSTR)calloc(2*MAX_PATH, sizeof(WCHAR));
|
|
if (path == NULL) { show_error(L"Out of memory"); return NULL; }
|
|
|
|
int image = 0;
|
|
BROWSEINFO bi = { NULL, NULL, name,
|
|
L"Select the folder where you want to install or update Calibre Portable",
|
|
BIF_RETURNONLYFSDIRS | BIF_DONTGOBELOWDOMAIN | BIF_USENEWUI,
|
|
NULL, NULL, image };
|
|
|
|
ret = SHBrowseForFolder(&bi);
|
|
if (ret == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
if (!SHGetPathFromIDList(ret, path)) {
|
|
show_detailed_error(L"The selected folder is not valid: ", name, 0);
|
|
return NULL;
|
|
}
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
static bool is_dots(LPCWSTR name) {
|
|
return wcscmp(name, L".") == 0 || wcscmp(name, L"..") == 0;
|
|
}
|
|
|
|
static bool rmtree(LPCWSTR path) {
|
|
SHFILEOPSTRUCTW op;
|
|
WCHAR buf[4*MAX_PATH + 2] = {0};
|
|
|
|
if (GetFullPathName(path, 4*MAX_PATH, buf, NULL) == 0) return false;
|
|
|
|
op.hwnd = NULL;
|
|
op.wFunc = FO_DELETE;
|
|
op.pFrom = buf;
|
|
op.pTo = NULL;
|
|
op.fFlags = FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT | FOF_NOCONFIRMMKDIR;
|
|
op.fAnyOperationsAborted = false;
|
|
op.hNameMappings = NULL;
|
|
op.lpszProgressTitle = NULL;
|
|
|
|
return SHFileOperationW(&op) == 0;
|
|
}
|
|
|
|
static BOOL find_portable_dir(LPCWSTR base, LPWSTR *result, BOOL *existing) {
|
|
WCHAR buf[4*MAX_PATH] = {0};
|
|
|
|
_snwprintf_s(buf, 4*MAX_PATH, _TRUNCATE, L"%s\\calibre-portable.exe", base);
|
|
*existing = true;
|
|
|
|
if (file_exists(buf)) {
|
|
*result = _wcsdup(base);
|
|
if (*result == NULL) { show_error(L"Out of memory"); return false; }
|
|
return true;
|
|
}
|
|
|
|
WIN32_FIND_DATA fdFile;
|
|
HANDLE hFind = NULL;
|
|
_snwprintf_s(buf, 4*MAX_PATH, _TRUNCATE, L"%s\\*", base);
|
|
|
|
if((hFind = FindFirstFileEx(buf, FindExInfoStandard, &fdFile, FindExSearchLimitToDirectories, NULL, 0)) != INVALID_HANDLE_VALUE) {
|
|
do {
|
|
if(is_dots(fdFile.cFileName)) continue;
|
|
|
|
if(fdFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
|
_snwprintf_s(buf, 4*MAX_PATH, _TRUNCATE, L"%s\\%s\\calibre-portable.exe", base, fdFile.cFileName);
|
|
if (file_exists(buf)) {
|
|
*result = _wcsdup(buf);
|
|
if (*result == NULL) { show_error(L"Out of memory"); return false; }
|
|
PathRemoveFileSpec(*result);
|
|
FindClose(hFind);
|
|
return true;
|
|
}
|
|
}
|
|
} while(FindNextFile(hFind, &fdFile));
|
|
FindClose(hFind);
|
|
}
|
|
|
|
*existing = false;
|
|
_snwprintf_s(buf, 4*MAX_PATH, _TRUNCATE, L"%s\\Calibre Portable", base);
|
|
if (!CreateDirectory(buf, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) {
|
|
show_last_error(L"Failed to create Calibre Portable folder");
|
|
return false;
|
|
}
|
|
*result = _wcsdup(buf);
|
|
if (*result == NULL) { show_error(L"Out of memory"); return false; }
|
|
|
|
return true;
|
|
}
|
|
|
|
static LPWSTR make_unpack_dir() {
|
|
WCHAR buf[4*MAX_PATH] = {0};
|
|
LPWSTR ans = NULL;
|
|
|
|
if (directory_exists(L"_unpack_calibre_portable"))
|
|
rmtree(L"_unpack_calibre_portable");
|
|
|
|
if (!CreateDirectory(L"_unpack_calibre_portable", NULL) && GetLastError() != ERROR_ALREADY_EXISTS) {
|
|
show_last_error(L"Failed to create temporary folder to unpack into");
|
|
return ans;
|
|
}
|
|
|
|
if (!GetFullPathName(L"_unpack_calibre_portable", 4*MAX_PATH, buf, NULL)) {
|
|
show_last_error(L"Failed to resolve path");
|
|
return NULL;
|
|
}
|
|
|
|
ans = _wcsdup(buf);
|
|
if (ans == NULL) show_error(L"Out of memory");
|
|
return ans;
|
|
|
|
}
|
|
|
|
static BOOL move_program() {
|
|
if (MoveFileEx(L"Calibre Portable\\calibre-portable.exe",
|
|
L"..\\calibre-portable.exe", MOVEFILE_REPLACE_EXISTING) == 0) {
|
|
show_last_error(L"Failed to move calibre-portable.exe, make sure calibre is not running");
|
|
return false;
|
|
}
|
|
|
|
if (directory_exists(L"..\\Calibre")) {
|
|
if (!rmtree(L"..\\Calibre")) {
|
|
show_error(L"Failed to delete the Calibre program folder. Make sure calibre is not running.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (MoveFileEx(L"Calibre Portable\\Calibre", L"..\\Calibre", 0) == 0) {
|
|
Sleep(4000); // Sleep and try again
|
|
if (MoveFileEx(L"Calibre Portable\\Calibre", L"..\\Calibre", 0) == 0) {
|
|
show_last_error(L"Failed to move calibre program folder. This is usually caused by an antivirus program or a file sync program like DropBox. Turn them off temporarily and try again. Underlying error: ");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!directory_exists(L"..\\Calibre Library")) {
|
|
MoveFileEx(L"Calibre Portable\\Calibre Library", L"..\\Calibre Library", 0);
|
|
}
|
|
|
|
if (!directory_exists(L"..\\Calibre Settings")) {
|
|
MoveFileEx(L"Calibre Portable\\Calibre Settings", L"..\\Calibre Settings", 0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
// }}}
|
|
|
|
static BOOL ensure_not_running(LPCWSTR dest) {
|
|
DWORD processes[4096], needed, num;
|
|
unsigned int i;
|
|
WCHAR name[4*MAX_PATH] = L"<unknown>";
|
|
HANDLE h;
|
|
DWORD len;
|
|
LPWSTR fname = NULL;
|
|
|
|
if ( !EnumProcesses( processes, sizeof(processes), &needed ) ) {
|
|
return true;
|
|
}
|
|
num = needed / sizeof(DWORD);
|
|
|
|
for (i = 0; i < num; i++) {
|
|
if (processes[i] == 0) continue;
|
|
h = OpenProcess( PROCESS_QUERY_INFORMATION, FALSE, processes[i] );
|
|
if (h != NULL) {
|
|
len = GetProcessImageFileNameW(h, name, 4*MAX_PATH);
|
|
CloseHandle(h);
|
|
if (len != 0) {
|
|
name[len] = 0;
|
|
fname = PathFindFileName(name);
|
|
if (wcscmp(fname, L"calibre.exe") == 0) {
|
|
show_error(L"Calibre appears to be running on your computer. Please quit it before trying to install Calibre Portable.");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void launch_calibre() {
|
|
STARTUPINFO si;
|
|
PROCESS_INFORMATION pi;
|
|
|
|
ZeroMemory( &si, sizeof(si) );
|
|
si.cb = sizeof(si);
|
|
ZeroMemory( &pi, sizeof(pi) );
|
|
|
|
|
|
if (CreateProcess(_wcsdup(L"calibre-portable.exe"), NULL,
|
|
NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP,
|
|
NULL, NULL, &si, &pi)
|
|
== 0) {
|
|
show_last_error(L"Failed to launch calibre portable");
|
|
}
|
|
|
|
// Close process and thread handles.
|
|
CloseHandle( pi.hProcess );
|
|
CloseHandle( pi.hThread );
|
|
|
|
}
|
|
|
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
|
|
{
|
|
|
|
LPVOID cdata = NULL;
|
|
DWORD csz = 0;
|
|
int ret = 0, argc;
|
|
HRESULT hr;
|
|
LPWSTR tgt = NULL, dest = NULL, *argv, unpack_dir = NULL;
|
|
BOOL existing = false, launch = false, automated = false;
|
|
WCHAR buf[4*MAX_PATH] = {0}, mb_msg[4*MAX_PATH] = {0}, fdest[4*MAX_PATH] = {0};
|
|
|
|
if (!load_data(&cdata, &csz)) return 0;
|
|
|
|
hr = CoInitialize(NULL);
|
|
if (FAILED(hr)) { show_error(L"Failed to initialize COM"); return 0; }
|
|
|
|
// Get the target directory for installation
|
|
argv = CommandLineToArgvW(GetCommandLine(), &argc);
|
|
if (argv == NULL) { show_last_error(L"Failed to get command line"); return 0; }
|
|
if (argc > 1) {
|
|
tgt = argv[1];
|
|
automated = true;
|
|
} else {
|
|
tgt = get_directory_from_user();
|
|
if (tgt == NULL) goto end;
|
|
}
|
|
|
|
if (!directory_exists(tgt)) {
|
|
show_detailed_error(L"The specified directory does not exist: ",
|
|
tgt, 1);
|
|
goto end;
|
|
}
|
|
|
|
// Ensure the path to Calibre Portable is not too long
|
|
do {
|
|
if (!find_portable_dir(tgt, &dest, &existing)) goto end;
|
|
|
|
if (GetFullPathName(dest, MAX_PATH*4, fdest, NULL) == 0) {
|
|
show_last_error(L"Failed to resolve target folder");
|
|
goto end;
|
|
}
|
|
free(dest); dest = NULL;
|
|
|
|
if (wcslen(fdest) > 58) {
|
|
_snwprintf_s(buf, 4*MAX_PATH, _TRUNCATE,
|
|
L"Path to Calibre Portable (%s) too long. Must be less than 59 characters.", fdest);
|
|
if (!existing) RemoveDirectory(fdest);
|
|
show_error(buf);
|
|
tgt = get_directory_from_user();
|
|
if (tgt == NULL) goto end;
|
|
}
|
|
} while (wcslen(fdest) > 58);
|
|
|
|
// Confirm the user wants to upgrade
|
|
if (existing && !automated) {
|
|
_snwprintf_s(mb_msg, 4*MAX_PATH, _TRUNCATE,
|
|
L"An existing install of Calibre Portable was found at %s. Do you want to upgrade it?",
|
|
fdest);
|
|
if (MessageBox(NULL, mb_msg,
|
|
L"Upgrade Calibre Portable?", MB_ICONEXCLAMATION | MB_YESNO | MB_TOPMOST) != IDYES)
|
|
goto end;
|
|
}
|
|
|
|
if (existing) {
|
|
if (!ensure_not_running(fdest)) goto end;
|
|
}
|
|
|
|
// Make a temp dir to unpack into
|
|
if (!SetCurrentDirectoryW(fdest)) { show_detailed_error(L"Failed to change to unzip directory: ", fdest, 0); goto end; }
|
|
|
|
if ( (unpack_dir = make_unpack_dir()) == NULL ) goto end;
|
|
if (!SetCurrentDirectoryW(unpack_dir)) { show_detailed_error(L"Failed to change to unpack directory: ", fdest, 0); goto end; }
|
|
|
|
// Extract files
|
|
if (!extract(cdata, csz)) goto end;
|
|
|
|
// Move files from temp dir to the install dir
|
|
if (!move_program()) goto end;
|
|
|
|
_snwprintf_s(mb_msg, 4*MAX_PATH, _TRUNCATE,
|
|
L"Calibre Portable successfully installed to %s. Launch calibre?",
|
|
fdest);
|
|
launch = MessageBox(NULL, mb_msg,
|
|
L"Success", MB_ICONINFORMATION | MB_YESNO | MB_TOPMOST) == IDYES;
|
|
|
|
end:
|
|
if (unpack_dir != NULL) { SetCurrentDirectoryW(L".."); rmtree(unpack_dir); free(unpack_dir); }
|
|
CoUninitialize();
|
|
if (launch) launch_calibre();
|
|
return 0;
|
|
}
|
|
|
|
|