mirror of
https://github.com/ioquake/ioq3.git
synced 2024-11-10 07:11:46 +00:00
Merge pull request #290 from rcgordon/autoupdater
Initial shot at writing an ioquake3 autoupdater. Thank you icculus, and everyone who contributed to his patreon! https://www.patreon.com/icculus
This commit is contained in:
commit
87eecd7bc1
7 changed files with 1468 additions and 22 deletions
101
Makefile
101
Makefile
|
@ -35,6 +35,9 @@ endif
|
|||
ifndef BUILD_RENDERER_OPENGL2
|
||||
BUILD_RENDERER_OPENGL2=
|
||||
endif
|
||||
ifndef BUILD_AUTOUPDATER # DON'T build unless you mean to!
|
||||
BUILD_AUTOUPDATER=0
|
||||
endif
|
||||
|
||||
#############################################################################
|
||||
#
|
||||
|
@ -228,6 +231,10 @@ ifndef USE_YACC
|
|||
USE_YACC=0
|
||||
endif
|
||||
|
||||
ifndef USE_AUTOUPDATER # DON'T include unless you mean to!
|
||||
USE_AUTOUPDATER=0
|
||||
endif
|
||||
|
||||
ifndef DEBUG_CFLAGS
|
||||
DEBUG_CFLAGS=-ggdb -O0
|
||||
endif
|
||||
|
@ -262,6 +269,7 @@ LBURGDIR=$(MOUNT_DIR)/tools/lcc/lburg
|
|||
Q3CPPDIR=$(MOUNT_DIR)/tools/lcc/cpp
|
||||
Q3LCCETCDIR=$(MOUNT_DIR)/tools/lcc/etc
|
||||
Q3LCCSRCDIR=$(MOUNT_DIR)/tools/lcc/src
|
||||
AUTOUPDATERSRCDIR=$(MOUNT_DIR)/autoupdater
|
||||
LOKISETUPDIR=misc/setup
|
||||
NSISDIR=misc/nsis
|
||||
SDLHDIR=$(MOUNT_DIR)/SDL2
|
||||
|
@ -269,29 +277,30 @@ LIBSDIR=$(MOUNT_DIR)/libs
|
|||
|
||||
bin_path=$(shell which $(1) 2> /dev/null)
|
||||
|
||||
# The autoupdater uses curl, so figure out its flags no matter what.
|
||||
# We won't need this if we only build the server
|
||||
ifneq ($(BUILD_CLIENT),0)
|
||||
# set PKG_CONFIG_PATH to influence this, e.g.
|
||||
# PKG_CONFIG_PATH=/opt/cross/i386-mingw32msvc/lib/pkgconfig
|
||||
ifneq ($(call bin_path, pkg-config),)
|
||||
CURL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags libcurl)
|
||||
CURL_LIBS ?= $(shell pkg-config --silence-errors --libs libcurl)
|
||||
OPENAL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags openal)
|
||||
OPENAL_LIBS ?= $(shell pkg-config --silence-errors --libs openal)
|
||||
SDL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags sdl2|sed 's/-Dmain=SDL_main//')
|
||||
SDL_LIBS ?= $(shell pkg-config --silence-errors --libs sdl2)
|
||||
FREETYPE_CFLAGS ?= $(shell pkg-config --silence-errors --cflags freetype2)
|
||||
else
|
||||
# assume they're in the system default paths (no -I or -L needed)
|
||||
CURL_LIBS ?= -lcurl
|
||||
OPENAL_LIBS ?= -lopenal
|
||||
endif
|
||||
# Use sdl2-config if all else fails
|
||||
ifeq ($(SDL_CFLAGS),)
|
||||
ifneq ($(call bin_path, sdl2-config),)
|
||||
SDL_CFLAGS ?= $(shell sdl2-config --cflags)
|
||||
SDL_LIBS ?= $(shell sdl2-config --libs)
|
||||
endif
|
||||
|
||||
# set PKG_CONFIG_PATH to influence this, e.g.
|
||||
# PKG_CONFIG_PATH=/opt/cross/i386-mingw32msvc/lib/pkgconfig
|
||||
ifneq ($(call bin_path, pkg-config),)
|
||||
CURL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags libcurl)
|
||||
CURL_LIBS ?= $(shell pkg-config --silence-errors --libs libcurl)
|
||||
OPENAL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags openal)
|
||||
OPENAL_LIBS ?= $(shell pkg-config --silence-errors --libs openal)
|
||||
SDL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags sdl2|sed 's/-Dmain=SDL_main//')
|
||||
SDL_LIBS ?= $(shell pkg-config --silence-errors --libs sdl2)
|
||||
FREETYPE_CFLAGS ?= $(shell pkg-config --silence-errors --cflags freetype2)
|
||||
else
|
||||
# assume they're in the system default paths (no -I or -L needed)
|
||||
CURL_LIBS ?= -lcurl
|
||||
OPENAL_LIBS ?= -lopenal
|
||||
endif
|
||||
|
||||
# Use sdl2-config if all else fails
|
||||
ifeq ($(SDL_CFLAGS),)
|
||||
ifneq ($(call bin_path, sdl2-config),)
|
||||
SDL_CFLAGS ?= $(shell sdl2-config --cflags)
|
||||
SDL_LIBS ?= $(shell sdl2-config --libs)
|
||||
endif
|
||||
endif
|
||||
|
||||
|
@ -367,6 +376,7 @@ ifneq (,$(findstring "$(PLATFORM)", "linux" "gnu_kfreebsd" "kfreebsd-gnu" "gnu")
|
|||
|
||||
THREAD_LIBS=-lpthread
|
||||
LIBS=-ldl -lm
|
||||
AUTOUPDATER_LIBS += -ldl
|
||||
|
||||
CLIENT_LIBS=$(SDL_LIBS)
|
||||
RENDERER_LIBS = $(SDL_LIBS) -lGL
|
||||
|
@ -594,6 +604,8 @@ ifdef MINGW
|
|||
endif
|
||||
|
||||
LIBS= -lws2_32 -lwinmm -lpsapi
|
||||
AUTOUPDATER_LIBS += -lwininet
|
||||
|
||||
# clang 3.4 doesn't support this
|
||||
ifneq ("$(CC)", $(findstring "$(CC)", "clang" "clang++"))
|
||||
CLIENT_LDFLAGS += -mwindows
|
||||
|
@ -834,6 +846,8 @@ ifeq ($(PLATFORM),irix64)
|
|||
SHLIBLDFLAGS=-shared
|
||||
|
||||
LIBS=-ldl -lm -lgen
|
||||
AUTOUPDATER_LIBS += -ldl
|
||||
|
||||
# FIXME: The X libraries probably aren't necessary?
|
||||
CLIENT_LIBS=-L/usr/X11/$(LIB) $(SDL_LIBS) \
|
||||
-lX11 -lXext -lm
|
||||
|
@ -888,6 +902,7 @@ ifeq ($(PLATFORM),sunos)
|
|||
|
||||
THREAD_LIBS=-lpthread
|
||||
LIBS=-lsocket -lnsl -ldl -lm
|
||||
AUTOUPDATER_LIBS += -ldl
|
||||
|
||||
BOTCFLAGS=-O0
|
||||
|
||||
|
@ -986,6 +1001,16 @@ ifneq ($(BUILD_GAME_QVM),0)
|
|||
endif
|
||||
endif
|
||||
|
||||
ifneq ($(BUILD_AUTOUPDATER),0)
|
||||
# PLEASE NOTE that if you run an exe on Windows Vista or later
|
||||
# with "setup", "install", "update" or other related terms, it
|
||||
# will unconditionally trigger a UAC prompt, and in the case of
|
||||
# ioq3 calling CreateProcess() on it, it'll just fail immediately.
|
||||
# So don't call this thing "autoupdater" here!
|
||||
AUTOUPDATER_BIN := autosyncerator$(FULLBINEXT)
|
||||
TARGETS += $(B)/$(AUTOUPDATER_BIN)
|
||||
endif
|
||||
|
||||
ifeq ($(USE_OPENAL),1)
|
||||
CLIENT_CFLAGS += -DUSE_OPENAL
|
||||
ifeq ($(USE_OPENAL_DLOPEN),1)
|
||||
|
@ -1086,6 +1111,11 @@ ifeq ($(USE_FREETYPE),1)
|
|||
RENDERER_LIBS += $(FREETYPE_LIBS)
|
||||
endif
|
||||
|
||||
ifeq ($(USE_AUTOUPDATER),1)
|
||||
CLIENT_CFLAGS += -DUSE_AUTOUPDATER -DAUTOUPDATER_BIN=\\\"$(AUTOUPDATER_BIN)\\\"
|
||||
SERVER_CFLAGS += -DUSE_AUTOUPDATER -DAUTOUPDATER_BIN=\\\"$(AUTOUPDATER_BIN)\\\"
|
||||
endif
|
||||
|
||||
ifeq ("$(CC)", $(findstring "$(CC)", "clang" "clang++"))
|
||||
BASE_CFLAGS += -Qunused-arguments
|
||||
endif
|
||||
|
@ -1317,6 +1347,9 @@ endif
|
|||
@echo " CLIENT_LIBS:"
|
||||
$(call print_wrapped, $(CLIENT_LIBS))
|
||||
@echo ""
|
||||
@echo " AUTOUPDATER_LIBS:"
|
||||
$(call print_wrapped, $(AUTOUPDATER_LIBS))
|
||||
@echo ""
|
||||
@echo " Output:"
|
||||
$(call print_list, $(NAKED_TARGETS))
|
||||
@echo ""
|
||||
|
@ -1342,6 +1375,7 @@ endif
|
|||
makedirs:
|
||||
@if [ ! -d $(BUILD_DIR) ];then $(MKDIR) $(BUILD_DIR);fi
|
||||
@if [ ! -d $(B) ];then $(MKDIR) $(B);fi
|
||||
@if [ ! -d $(B)/autoupdater ];then $(MKDIR) $(B)/autoupdater;fi
|
||||
@if [ ! -d $(B)/client ];then $(MKDIR) $(B)/client;fi
|
||||
@if [ ! -d $(B)/client/opus ];then $(MKDIR) $(B)/client/opus;fi
|
||||
@if [ ! -d $(B)/client/vorbis ];then $(MKDIR) $(B)/client/vorbis;fi
|
||||
|
@ -1561,6 +1595,27 @@ $(Q3ASM): $(Q3ASMOBJ)
|
|||
$(Q)$(TOOLS_CC) $(TOOLS_CFLAGS) $(TOOLS_LDFLAGS) -o $@ $^ $(TOOLS_LIBS)
|
||||
|
||||
|
||||
#############################################################################
|
||||
# AUTOUPDATER
|
||||
#############################################################################
|
||||
|
||||
define DO_AUTOUPDATER_CC
|
||||
$(echo_cmd) "AUTOUPDATER_CC $<"
|
||||
$(Q)$(CC) $(CFLAGS) $(CURL_CFLAGS) -o $@ -c $<
|
||||
endef
|
||||
|
||||
Q3AUTOUPDATEROBJ = \
|
||||
$(B)/autoupdater/autoupdater.o \
|
||||
$(B)/autoupdater/sha256.o
|
||||
|
||||
$(B)/autoupdater/%.o: $(AUTOUPDATERSRCDIR)/%.c
|
||||
$(DO_AUTOUPDATER_CC)
|
||||
|
||||
$(B)/$(AUTOUPDATER_BIN): $(Q3AUTOUPDATEROBJ)
|
||||
$(echo_cmd) "AUTOUPDATER_LD $@"
|
||||
$(Q)$(CC) $(LDFLAGS) -o $@ $(Q3AUTOUPDATEROBJ) $(AUTOUPDATER_LIBS)
|
||||
|
||||
|
||||
#############################################################################
|
||||
# CLIENT/SERVER
|
||||
#############################################################################
|
||||
|
@ -1664,6 +1719,7 @@ Q3OBJ = \
|
|||
$(B)/client/sdl_snd.o \
|
||||
\
|
||||
$(B)/client/con_log.o \
|
||||
$(B)/client/sys_autoupdater.o \
|
||||
$(B)/client/sys_main.o
|
||||
|
||||
ifdef MINGW
|
||||
|
@ -2200,6 +2256,7 @@ Q3DOBJ = \
|
|||
$(B)/ded/null_snddma.o \
|
||||
\
|
||||
$(B)/ded/con_log.o \
|
||||
$(B)/ded/sys_autoupdater.o \
|
||||
$(B)/ded/sys_main.o
|
||||
|
||||
ifeq ($(ARCH),x86)
|
||||
|
|
125
autoupdater-readme.txt
Normal file
125
autoupdater-readme.txt
Normal file
|
@ -0,0 +1,125 @@
|
|||
The updater program's code is public domain. The rest of ioquake3 is not.
|
||||
|
||||
The source code to the autoupdater is in the code/autoupdater directory.
|
||||
There is a small piece of code in ioquake3 itself at startup, too.
|
||||
|
||||
(This is all Unix terminology, but similar approaches on Windows apply.)
|
||||
|
||||
The updater is a separate program, written in C, with no dependencies on
|
||||
the game. It (statically) links to libcurl and uses the C runtime, but
|
||||
otherwise has no external dependencies. It has to be a single binary file
|
||||
with no shared libraries.
|
||||
|
||||
The basic flow looks like this:
|
||||
|
||||
- The game launches as usual.
|
||||
- Right after main() starts, the game creates a pipe, forks off a new process,
|
||||
and execs the updater in that process. The game won't ever touch the pipe
|
||||
again. It's just there to block the child app until the game terminates.
|
||||
- The updater has no UI. It writes a log file.
|
||||
- The updater downloads a manifest from a known URL over https://, using
|
||||
libCurl. The base URL is platform-specific (it might be
|
||||
https://example.com/mac/, or https://example.com/linux-x86/, whatever).
|
||||
The manifest is at $BASEURL/manifest.txt
|
||||
- The manifest looks like this: three lines per file...
|
||||
|
||||
Contents/MacOS/baseq3/uix86_64.dylib
|
||||
332428
|
||||
a49bbe77f8eb6c195265ea136f881f7830db58e4d8a883b27f59e1e23e396a20
|
||||
|
||||
- That's the file's path, its size in bytes, and an sha256 hash of the data.
|
||||
- The file will be at this path under the base url on the webserver.
|
||||
- The manifest only lists files that ever needed updating; it's not necessary
|
||||
to list every file in the game's installation (unless you want to allow the
|
||||
entire game to download).
|
||||
- The updater will check each item in the manifest:
|
||||
- Does the file not exist in the install? Needs downloading.
|
||||
- Does the file have a different size? Needs downloading.
|
||||
- Does the file have a different sha256sum? Needs downloading.
|
||||
- Otherwise, file is up to date, leave it alone.
|
||||
- If an item needs downloading, do these same checks against the file in the
|
||||
download directory (if it's already there and matches, don't download again.)
|
||||
- Download necessary files with libcurl, put it in a download directory.
|
||||
- The downloaded file is also checked for size and sha256 vs the manifest, to
|
||||
make sure there was no corruption or confusion. If a downloaded file doesn't
|
||||
match what was expected, the updater aborts and will try again next time.
|
||||
This could fail checksum due to i/o errors and compromised security, but
|
||||
it might just be that a new version was being published and bad luck
|
||||
happened, and a retry later could correct everything.
|
||||
- If the updater itself needs upgrading, we deal with that first. It's
|
||||
downloaded, then the updater relaunches from the downloaded binary with
|
||||
a special command line. That relaunched process copies itself to the proper
|
||||
location, and then relaunches _again_ to restart the normal updating
|
||||
process with the new updater in its correct position.
|
||||
- Once the downloads are complete and the updater itself doesn't need
|
||||
upgrading, we are ready to start the normal upgrade. Since we can't replace
|
||||
executables on some platforms while they are running, and swapping out a
|
||||
game's data files at runtime isn't wise in general, the updater will now
|
||||
block until the game terminates. It does this by reading on the pipe that
|
||||
the game created when forking the updater; since the game never writes
|
||||
anything to this pipe, it causes the updater to block until the pipe closes.
|
||||
Since the game never deliberately closes the pipe either, it remains open
|
||||
until the OS forcibly closes it as the game process terminates. Being an
|
||||
unnamed pipe, it just vaporizes at this point, leaving no state that might
|
||||
accidentally hang us up later, like a global semaphore or whatnot. This
|
||||
technique also lets us localize the game's code changes to one small block
|
||||
of C code, with no need to manage these resources elsewhere.
|
||||
- As a sanity check, the updater will also kill(game_process_id, 0) until it
|
||||
fails, sleeping for 100 milliseconds between each attempt, in case the
|
||||
process is still being cleaned up by the OS after closing the pipe.
|
||||
- Once the updater is confident the game process is gone, it will start
|
||||
upgrading the appropriate files. It does this in two steps: it moves
|
||||
the old file to a "rollback" directory so it's out of the way but still
|
||||
available, then it moves the newly-downloaded file into place. Since these
|
||||
are all simple renames and not copies, this can move fast. Any missing
|
||||
parent directories are created, in case the update is adding a new file
|
||||
in a directory that didn't previously exist.
|
||||
- If something goes wrong at this point (file i/o error, etc), the updater
|
||||
will roll back the changes by deleting the updated files, and moving the
|
||||
files in the "rollback" directory back to their original locations. Then
|
||||
the updater aborts.
|
||||
- If nothing went wrong, the rollback files are deleted. And we are officially
|
||||
up to date! The updater terminates.
|
||||
|
||||
|
||||
The updater is designed to fail at any point. If a download fails, it'll
|
||||
pick up and try again next time, etc. Completed downloads will remain, so it
|
||||
will just need to download any missing/incomplete files.
|
||||
|
||||
The server side just needs to be able to serve static files over HTTPS from
|
||||
any standard Apache/nginx/whatever process.
|
||||
|
||||
Failure points:
|
||||
- If the updater fails when still downloading data, it just picks up on next
|
||||
restart.
|
||||
- If the updater fails when replacing files, it rolls back any changes it has
|
||||
made.
|
||||
- If the updater fails when rolling back, then running the updater again after
|
||||
fixing the specific problem (disk error, etc?) will redownload and replace
|
||||
any files that were left in an uncertain state. The only true point of
|
||||
risk is crashing during a rollback and then having the updater bricked for
|
||||
some reason, but that's an extremely small surface area, knock on wood.
|
||||
- If the updater crashes or totally bricks, ioquake3 should just keep being
|
||||
ioquake3. It will still launch and play, even if the updater is quietly
|
||||
segfaulting in the background on startup.
|
||||
- If an update bricks ioquake3 to the point where it can't run the updater,
|
||||
running the updater directly should let it recover (assuming a future update
|
||||
fixes the problem).
|
||||
|
||||
|
||||
Items to consider for future revisions:
|
||||
- GPG sign the manifest; if we can be confident that the manifest isn't
|
||||
compromised, then the sha256 hashes of each file it contains should protect
|
||||
the rest of the process. As it currently stands, we trust the download
|
||||
server isn't compromised.
|
||||
- Maybe put a limit on the number manifest downloads, so we only check once
|
||||
every hour? Every day?
|
||||
- Channels? Stable (what everyone gets by default), Nightly (once a day),
|
||||
Experimental (some other work-in-progress branch), Bloody (literally the
|
||||
latest commit).
|
||||
- Let mods update, separate from the main game?
|
||||
|
||||
Questions? Ask Ryan: icculus@icculus.org
|
||||
|
||||
--ryan.
|
||||
|
977
code/autoupdater/autoupdater.c
Normal file
977
code/autoupdater/autoupdater.c
Normal file
|
@ -0,0 +1,977 @@
|
|||
/*
|
||||
The code in this file is in the public domain. The rest of ioquake3
|
||||
is licensed under the GPLv2. Do not mingle code, please!
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#if defined(_MSC_VER) && (_MSC_VER < 1600)
|
||||
typedef __int64 int64_t;
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN 1
|
||||
#include <windows.h>
|
||||
#include <wininet.h>
|
||||
#include <tlhelp32.h>
|
||||
#define PIDFMT "%u"
|
||||
#define PIDFMTCAST unsigned int
|
||||
typedef DWORD PID;
|
||||
#else
|
||||
#include <signal.h>
|
||||
#include <curl/curl.h>
|
||||
typedef pid_t PID;
|
||||
#define PIDFMT "%llu"
|
||||
#define PIDFMTCAST unsigned long long
|
||||
#endif
|
||||
|
||||
#include "sha256.h"
|
||||
|
||||
|
||||
#ifndef AUTOUPDATE_USER_AGENT
|
||||
#define AUTOUPDATE_USER_AGENT "ioq3autoupdater/0.1"
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef AUTOUPDATE_URL
|
||||
|
||||
#ifndef AUTOUPDATE_BASEURL
|
||||
#define AUTOUPDATE_BASEURL "https://upd.ioquake3.org/updates/v1"
|
||||
#endif
|
||||
|
||||
#ifndef AUTOUPDATE_PACKAGE
|
||||
#define AUTOUPDATE_PACKAGE "ioquake3"
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
#define AUTOUPDATE_PLATFORM "mac"
|
||||
#elif defined(__linux__)
|
||||
#define AUTOUPDATE_PLATFORM "linux"
|
||||
#elif defined(_WIN32)
|
||||
#define AUTOUPDATE_PLATFORM "windows"
|
||||
#else
|
||||
#error Please define your platform.
|
||||
#endif
|
||||
|
||||
#ifdef __i386__
|
||||
#define AUTOUPDATE_ARCH "x86"
|
||||
#elif defined(__x86_64__)
|
||||
#define AUTOUPDATE_ARCH "x86-64"
|
||||
#else
|
||||
#error Please define your platform.
|
||||
#endif
|
||||
|
||||
#define AUTOUPDATE_URL AUTOUPDATE_BASEURL "/" AUTOUPDATE_PACKAGE "/" AUTOUPDATE_PLATFORM "/" AUTOUPDATE_ARCH "/"
|
||||
#endif
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define NEVER_RETURNS __attribute__((noreturn))
|
||||
#else
|
||||
#define NEVER_RETURNS
|
||||
#endif
|
||||
|
||||
|
||||
typedef struct
|
||||
{
|
||||
pid_t waitforprocess;
|
||||
const char *updateself;
|
||||
} Options;
|
||||
|
||||
static Options options;
|
||||
|
||||
|
||||
typedef struct ManifestItem
|
||||
{
|
||||
char *fname;
|
||||
unsigned char sha256[32];
|
||||
int64_t len;
|
||||
int update;
|
||||
int rollback;
|
||||
struct ManifestItem *next;
|
||||
} ManifestItem;
|
||||
|
||||
static ManifestItem *manifest = NULL;
|
||||
|
||||
static void freeManifest(void)
|
||||
{
|
||||
ManifestItem *item = manifest;
|
||||
manifest = NULL;
|
||||
|
||||
while (item != NULL) {
|
||||
ManifestItem *next = item->next;
|
||||
free(item->fname);
|
||||
free(item);
|
||||
item = next;
|
||||
}
|
||||
manifest = NULL;
|
||||
}
|
||||
|
||||
static const char *timestamp(void)
|
||||
{
|
||||
time_t t = time(NULL);
|
||||
char *retval = asctime(localtime(&t));
|
||||
if (retval) {
|
||||
char *ptr;
|
||||
for (ptr = retval; *ptr; ptr++) {
|
||||
if ((*ptr == '\r') || (*ptr == '\n')) {
|
||||
*ptr = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return retval ? retval : "[date unknown]";
|
||||
}
|
||||
|
||||
|
||||
static FILE *logfile = NULL;
|
||||
|
||||
#define SDL_PRINTF_VARARG_FUNC( fmtargnumber ) __attribute__ (( format( __printf__, fmtargnumber, fmtargnumber+1 )))
|
||||
|
||||
static void info(const char *str)
|
||||
{
|
||||
fputs(str, logfile);
|
||||
fputs("\n", logfile);
|
||||
fflush(logfile);
|
||||
}
|
||||
|
||||
static void infof(const char *fmt, ...)
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
__attribute__ (( format( __printf__, 1, 2 )))
|
||||
#endif
|
||||
;
|
||||
|
||||
static void infof(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vfprintf(logfile, fmt, ap);
|
||||
va_end(ap);
|
||||
fputs("\n", logfile);
|
||||
fflush(logfile);
|
||||
}
|
||||
|
||||
static void restoreRollbacks(void)
|
||||
{
|
||||
/* you can't call die() in this function! If this fails, you're doomed. */
|
||||
ManifestItem *item;
|
||||
for (item = manifest; item != NULL; item = item->next) {
|
||||
if (item->rollback) {
|
||||
char rollbackPath[64];
|
||||
snprintf(rollbackPath, sizeof (rollbackPath), "updates/rollbacks/%d", item->rollback);
|
||||
infof("restore rollback: '%s' -> '%s'", rollbackPath, item->fname);
|
||||
remove(item->fname);
|
||||
rename(rollbackPath, item->fname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void die(const char *why) NEVER_RETURNS;
|
||||
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#define chmod(a,b) do {} while (0)
|
||||
#define makeDir(path) mkdir(path)
|
||||
|
||||
static void windowsWaitForProcessToDie(const DWORD pid)
|
||||
{
|
||||
HANDLE h;
|
||||
infof("Waiting on process ID #%u", (unsigned int) pid);
|
||||
h = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
||||
if (!h) {
|
||||
const DWORD err = GetLastError();
|
||||
if (err == ERROR_INVALID_PARAMETER) {
|
||||
info("No such process; probably already dead. Carry on.");
|
||||
return; /* process is (probably) already gone. */
|
||||
}
|
||||
infof("OpenProcess failed. err=%d", (unsigned int) err);
|
||||
die("OpenProcess failed");
|
||||
}
|
||||
if (WaitForSingleObject(h, INFINITE) != WAIT_OBJECT_0) {
|
||||
die("WaitForSingleObject failed");
|
||||
}
|
||||
CloseHandle(h);
|
||||
}
|
||||
|
||||
static void launchProcess(const char *exe, ...)
|
||||
{
|
||||
PROCESS_INFORMATION procinfo;
|
||||
STARTUPINFO startinfo;
|
||||
va_list ap;
|
||||
char cmdline[1024];
|
||||
char *ptr = cmdline;
|
||||
size_t totallen = 0;
|
||||
const char *arg = NULL;
|
||||
|
||||
#define APPENDCMDLINE(str) { \
|
||||
const size_t len = strlen(str); \
|
||||
totallen += len; \
|
||||
if ((totallen + 1) < sizeof (cmdline)) { \
|
||||
strcpy(ptr, str); \
|
||||
ptr += len; \
|
||||
} \
|
||||
}
|
||||
|
||||
va_start(ap, exe);
|
||||
APPENDCMDLINE(exe);
|
||||
while ((arg = va_arg(ap, const char *)) != NULL) {
|
||||
APPENDCMDLINE(arg);
|
||||
}
|
||||
va_end(ap);
|
||||
|
||||
if (totallen >= sizeof (cmdline)) {
|
||||
die("command line too long to launch.");
|
||||
}
|
||||
|
||||
cmdline[totallen] = 0;
|
||||
|
||||
infof("launching process '%s' with cmdline '%s'", exe, cmdline);
|
||||
|
||||
memset(&procinfo, '\0', sizeof (procinfo));
|
||||
memset(&startinfo, '\0', sizeof (startinfo));
|
||||
startinfo.cb = sizeof (startinfo);
|
||||
if (CreateProcessA(exe, cmdline, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &startinfo, &procinfo))
|
||||
{
|
||||
CloseHandle(procinfo.hProcess);
|
||||
CloseHandle(procinfo.hThread);
|
||||
exit(0); /* we're done, it's launched. */
|
||||
}
|
||||
|
||||
infof("CreateProcess failed: err=%d", (int) GetLastError());
|
||||
}
|
||||
|
||||
static HINTERNET hInternet;
|
||||
static void prepHttpLib(void)
|
||||
{
|
||||
hInternet = InternetOpenA(AUTOUPDATE_USER_AGENT,
|
||||
INTERNET_OPEN_TYPE_PRECONFIG,
|
||||
NULL, NULL, 0);
|
||||
if (!hInternet) {
|
||||
die("InternetOpen failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void shutdownHttpLib(void)
|
||||
{
|
||||
if (hInternet) {
|
||||
InternetCloseHandle(hInternet);
|
||||
hInternet = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static int runHttpDownload(const char *from, FILE *to)
|
||||
{
|
||||
/* !!! FIXME: some of this could benefit from GetLastError+FormatMessage. */
|
||||
int retval = 0;
|
||||
DWORD httpcode = 0;
|
||||
DWORD dwordlen = sizeof (DWORD);
|
||||
DWORD zero = 0;
|
||||
HINTERNET hUrl = InternetOpenUrlA(hInternet, from, NULL, 0,
|
||||
INTERNET_FLAG_HYPERLINK |
|
||||
INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP |
|
||||
INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS |
|
||||
INTERNET_FLAG_NO_CACHE_WRITE |
|
||||
INTERNET_FLAG_NO_COOKIES |
|
||||
INTERNET_FLAG_NO_UI |
|
||||
INTERNET_FLAG_RESYNCHRONIZE |
|
||||
INTERNET_FLAG_RELOAD |
|
||||
INTERNET_FLAG_SECURE, 0);
|
||||
|
||||
if (!hUrl) {
|
||||
infof("InternetOpenUrl failed. err=%d", (int) GetLastError());
|
||||
} else if (!HttpQueryInfo(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &httpcode, &dwordlen, &zero)) {
|
||||
infof("HttpQueryInfo failed. err=%d", (int) GetLastError());
|
||||
} else if (httpcode != 200) {
|
||||
infof("HTTP request failed with response code %d", (int) httpcode);
|
||||
} else {
|
||||
while (1) {
|
||||
DWORD br = 0;
|
||||
BYTE buf[1024 * 64];
|
||||
if (!InternetReadFile(hUrl, buf, sizeof (buf), &br)) {
|
||||
infof("InternetReadFile failed. err=%d", (int) GetLastError());
|
||||
break;
|
||||
} else if (br == 0) {
|
||||
retval = 1;
|
||||
break; /* done! */
|
||||
} else {
|
||||
if (fwrite(buf, br, 1, to) != 1) {
|
||||
info("fwrite failed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InternetCloseHandle(hUrl);
|
||||
return retval;
|
||||
}
|
||||
|
||||
#else /* Everything that isn't Windows. */
|
||||
|
||||
#define launchProcess execl
|
||||
#define makeDir(path) mkdir(path, 0777)
|
||||
|
||||
/* hooray for Unix linker hostility! */
|
||||
#undef curl_easy_setopt
|
||||
#include <dlfcn.h>
|
||||
typedef void (*CURLFN_curl_easy_cleanup)(CURL *curl);
|
||||
typedef CURL *(*CURLFN_curl_easy_init)(void);
|
||||
typedef CURLcode (*CURLFN_curl_easy_setopt)(CURL *curl, CURLoption option, ...);
|
||||
typedef CURLcode (*CURLFN_curl_easy_perform)(CURL *curl);
|
||||
typedef CURLcode (*CURLFN_curl_global_init)(long flags);
|
||||
typedef void (*CURLFN_curl_global_cleanup)(void);
|
||||
|
||||
static CURLFN_curl_easy_cleanup CURL_curl_easy_cleanup;
|
||||
static CURLFN_curl_easy_init CURL_curl_easy_init;
|
||||
static CURLFN_curl_easy_setopt CURL_curl_easy_setopt;
|
||||
static CURLFN_curl_easy_perform CURL_curl_easy_perform;
|
||||
static CURLFN_curl_global_init CURL_curl_global_init;
|
||||
static CURLFN_curl_global_cleanup CURL_curl_global_cleanup;
|
||||
|
||||
static void prepHttpLib(void)
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
const char *libname = "libcurl.4.dylib";
|
||||
#else
|
||||
const char *libname = "libcurl.so.4";
|
||||
#endif
|
||||
|
||||
void *handle = dlopen(libname, RTLD_NOW | RTLD_GLOBAL);
|
||||
if (!handle) {
|
||||
infof("dlopen(\"%s\") failed: %s", libname, dlerror());
|
||||
die("Failed to load libcurl library");
|
||||
}
|
||||
#define LOADCURLSYM(fn) \
|
||||
if ((CURL_##fn = (CURLFN_##fn) dlsym(handle, #fn)) == NULL) { \
|
||||
die("Failed to load libcurl symbol '" #fn "'"); \
|
||||
}
|
||||
|
||||
LOADCURLSYM(curl_easy_cleanup);
|
||||
LOADCURLSYM(curl_easy_init);
|
||||
LOADCURLSYM(curl_easy_setopt);
|
||||
LOADCURLSYM(curl_easy_perform);
|
||||
LOADCURLSYM(curl_global_init);
|
||||
LOADCURLSYM(curl_global_cleanup);
|
||||
|
||||
#define curl_easy_cleanup CURL_curl_easy_cleanup
|
||||
#define curl_easy_init CURL_curl_easy_init
|
||||
#define curl_easy_setopt CURL_curl_easy_setopt
|
||||
#define curl_easy_perform CURL_curl_easy_perform
|
||||
#define curl_global_init CURL_curl_global_init
|
||||
#define curl_global_cleanup CURL_curl_global_cleanup
|
||||
|
||||
if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) {
|
||||
die("curl_global_init() failed!");
|
||||
}
|
||||
}
|
||||
|
||||
static void shutdownHttpLib(void)
|
||||
{
|
||||
if (curl_global_cleanup) {
|
||||
curl_global_cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
static int runHttpDownload(const char *from, FILE *to)
|
||||
{
|
||||
int retval;
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
info("curl_easy_init() failed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if 0
|
||||
/* !!! FIXME: enable compression? */
|
||||
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); /* enable compression */
|
||||
|
||||
/* !!! FIXME; hook up proxy support to libcurl */
|
||||
curl_easy_setopt(curl, CURLOPT_PROXY, proxyURL);
|
||||
#endif
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_STDERR, logfile);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, to);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, from);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); /* allow redirects. */
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, AUTOUPDATE_USER_AGENT);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); /* require valid SSL cert. */
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); /* require SSL cert with same hostname as we connected to. */
|
||||
|
||||
retval = (curl_easy_perform(curl) == CURLE_OK);
|
||||
curl_easy_cleanup(curl);
|
||||
return retval;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
static void die(const char *why)
|
||||
{
|
||||
infof("FAILURE: %s", why);
|
||||
restoreRollbacks();
|
||||
freeManifest();
|
||||
infof("Updater ending (in failure), %s", timestamp());
|
||||
exit(1);
|
||||
}
|
||||
|
||||
static void outOfMemory(void) NEVER_RETURNS;
|
||||
static void outOfMemory(void)
|
||||
{
|
||||
die("Out of memory");
|
||||
}
|
||||
|
||||
static void buildParentDirs(const char *_path)
|
||||
{
|
||||
char *ptr;
|
||||
char *path = (char *) alloca(strlen(_path) + 1);
|
||||
if (!path) {
|
||||
outOfMemory();
|
||||
}
|
||||
strcpy(path, _path);
|
||||
|
||||
for (ptr = path; *ptr; ptr++) {
|
||||
if (*ptr == '/') {
|
||||
*ptr = '\0';
|
||||
makeDir(path);
|
||||
*ptr = '/';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int64_t fileLength(const char *fname)
|
||||
{
|
||||
struct stat statbuf;
|
||||
if (stat(fname, &statbuf) == -1) {
|
||||
return -1;
|
||||
}
|
||||
return (int64_t) statbuf.st_size;
|
||||
}
|
||||
|
||||
static void parseArgv(int argc, char **argv)
|
||||
{
|
||||
int i;
|
||||
|
||||
infof("command line (argc=%d)...", argc);
|
||||
for (i = 0; i < argc; i++) {
|
||||
infof(" argv[%d]: %s",i, argv[i]);
|
||||
}
|
||||
|
||||
for (i = 1; i < argc; i += 2) {
|
||||
if (strcmp(argv[i], "--waitpid") == 0) {
|
||||
options.waitforprocess = atoll(argv[i + 1]);
|
||||
infof("We will wait for process " PIDFMT " if necessary", (PIDFMTCAST) options.waitforprocess);
|
||||
} else if (strcmp(argv[i], "--updateself") == 0) {
|
||||
options.updateself = argv[i + 1];
|
||||
infof("We are updating ourself ('%s')", options.updateself);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void downloadURL(const char *from, const char *to)
|
||||
{
|
||||
FILE *io = NULL;
|
||||
const size_t len = strlen(AUTOUPDATE_URL) + strlen(from) + 1;
|
||||
char *fullurl = (char *) alloca(len);
|
||||
if (!fullurl) {
|
||||
outOfMemory();
|
||||
}
|
||||
snprintf(fullurl, len, "%s%s", AUTOUPDATE_URL, from);
|
||||
|
||||
infof("Downloading from '%s' to '%s'", fullurl, to);
|
||||
|
||||
buildParentDirs(to);
|
||||
io = fopen(to, "wb");
|
||||
if (!io) {
|
||||
die("Failed to open output file");
|
||||
}
|
||||
|
||||
if (!runHttpDownload(fullurl, io)) {
|
||||
fclose(io);
|
||||
remove(to);
|
||||
die("Download failed");
|
||||
}
|
||||
|
||||
if (fclose(io) == EOF) {
|
||||
die("Can't flush file on close. i/o error? Disk full?");
|
||||
}
|
||||
|
||||
chmod(to, 0777); /* !!! FIXME */
|
||||
}
|
||||
|
||||
static int hexcvt(const int ch)
|
||||
{
|
||||
if ((ch >= 'a') && (ch <= 'f')) {
|
||||
return (ch - 'a') + 10;
|
||||
} else if ((ch >= 'A') && (ch <= 'F')) {
|
||||
return (ch - 'A') + 10;
|
||||
} else if ((ch >= '0') && (ch <= '9')) {
|
||||
return ch - '0';
|
||||
} else {
|
||||
die("Invalid hex character");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void convertSha256(char *str, uint8 *sha256)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 32; i++) {
|
||||
const int a = hexcvt(*(str++));
|
||||
const int b = hexcvt(*(str++));
|
||||
*sha256 = (a << 4) | b;
|
||||
sha256++;
|
||||
}
|
||||
}
|
||||
|
||||
static void parseManifest(const char *fname)
|
||||
{
|
||||
ManifestItem *item = NULL;
|
||||
FILE *io = fopen(fname, "r");
|
||||
char buf[512];
|
||||
if (!io) {
|
||||
die("Failed to open manifest for reading");
|
||||
}
|
||||
|
||||
/* !!! FIXME: this code sucks. */
|
||||
while (fgets(buf, sizeof (buf), io)) {
|
||||
char *ptr = (buf + strlen(buf)) - 1;
|
||||
while (ptr >= buf) {
|
||||
if ((*ptr != '\n') && (*ptr != '\r')) {
|
||||
break;
|
||||
}
|
||||
*ptr = '\0';
|
||||
ptr--;
|
||||
}
|
||||
|
||||
if (!item && !buf[0]) {
|
||||
continue; /* blank line between items or blank at EOF */
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
infof("Next manifest item: %s", buf);
|
||||
|
||||
item = (ManifestItem *) calloc(1, sizeof (ManifestItem));
|
||||
if (!item) {
|
||||
outOfMemory();
|
||||
}
|
||||
item->fname = strdup(buf);
|
||||
if (!item->fname) {
|
||||
outOfMemory();
|
||||
}
|
||||
item->len = -1;
|
||||
item->next = NULL;
|
||||
} else if (item->len == -1) {
|
||||
infof("Item size: %s", buf);
|
||||
item->len = atoll(buf);
|
||||
} else {
|
||||
infof("Item sha256: %s", buf);
|
||||
convertSha256(buf, item->sha256);
|
||||
item->next = manifest;
|
||||
manifest = item;
|
||||
item = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (ferror(io)) {
|
||||
die("Error reading manifest");
|
||||
} else if (item) {
|
||||
die("Incomplete manifest");
|
||||
}
|
||||
|
||||
fclose(io);
|
||||
}
|
||||
|
||||
static void downloadManifest(void)
|
||||
{
|
||||
const char *manifestfname = "updates/manifest.txt";
|
||||
downloadURL("manifest.txt", manifestfname);
|
||||
/* !!! FIXME: verify manifest download is complete... */
|
||||
parseManifest(manifestfname);
|
||||
}
|
||||
|
||||
static void upgradeSelfAndRestart(const char *argv0) NEVER_RETURNS;
|
||||
static void upgradeSelfAndRestart(const char *argv0)
|
||||
{
|
||||
const char *tempfname = "origUpdater";
|
||||
const char *why = NULL;
|
||||
FILE *in = NULL;
|
||||
FILE *out = NULL;
|
||||
|
||||
/* unix replaces the process with execl(), but Windows needs to wait for the parent to terminate. */
|
||||
#ifdef _WIN32
|
||||
DWORD ppid = 0;
|
||||
HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (h) {
|
||||
const DWORD myPid = GetCurrentProcessId();
|
||||
PROCESSENTRY32 pe;
|
||||
memset(&pe, '\0', sizeof (pe));
|
||||
pe.dwSize = sizeof(PROCESSENTRY32);
|
||||
|
||||
if (Process32First(h, &pe)) {
|
||||
do {
|
||||
if (pe.th32ProcessID == myPid) {
|
||||
ppid = pe.th32ParentProcessID;
|
||||
break;
|
||||
}
|
||||
} while (Process32Next(h, &pe));
|
||||
}
|
||||
CloseHandle(h);
|
||||
}
|
||||
if (!ppid) {
|
||||
die("Can't determine parent process id");
|
||||
}
|
||||
windowsWaitForProcessToDie(ppid);
|
||||
#endif
|
||||
|
||||
in = fopen(argv0, "rb");
|
||||
if (!in) {
|
||||
die("Can't open self for input while upgrading updater");
|
||||
}
|
||||
|
||||
remove(tempfname);
|
||||
if (rename(options.updateself, tempfname) == -1) {
|
||||
die("Can't rename original while upgrading updater");
|
||||
}
|
||||
|
||||
out = fopen(options.updateself, "wb");
|
||||
if (!out) {
|
||||
die("Can't open file for output while upgrading updater");
|
||||
}
|
||||
|
||||
while (!feof(in) && !why) {
|
||||
char buf[512];
|
||||
const size_t br = fread(buf, 1, sizeof (buf), in);
|
||||
if (br > 0) {
|
||||
if (fwrite(buf, br, 1, out) != 1) {
|
||||
why = "write failure while upgrading updater";
|
||||
}
|
||||
} else if (ferror(in)) {
|
||||
why = "read failure while upgrading updater";
|
||||
}
|
||||
}
|
||||
|
||||
fclose(in);
|
||||
|
||||
if ((fclose(out) == EOF) && (!why)) {
|
||||
why = "close failure while upgrading updater";
|
||||
}
|
||||
|
||||
if (why) {
|
||||
remove(options.updateself);
|
||||
rename(tempfname, options.updateself);
|
||||
die(why);
|
||||
}
|
||||
|
||||
remove(tempfname);
|
||||
|
||||
chmod(options.updateself, 0777);
|
||||
|
||||
if (options.waitforprocess) {
|
||||
char pidstr[64];
|
||||
snprintf(pidstr, sizeof (pidstr), PIDFMT, (PIDFMTCAST) options.waitforprocess);
|
||||
launchProcess(options.updateself, options.updateself, "--waitpid", pidstr, NULL);
|
||||
} else {
|
||||
launchProcess(options.updateself, options.updateself, NULL);
|
||||
}
|
||||
die("Failed to relaunch upgraded updater");
|
||||
}
|
||||
|
||||
static const char *justFilename(const char *path)
|
||||
{
|
||||
const char *fname = strrchr(path, '/');
|
||||
return fname ? fname + 1 : path;
|
||||
}
|
||||
|
||||
static void hashFile(const char *fname, unsigned char *sha256)
|
||||
{
|
||||
SHA256_CTX sha256ctx;
|
||||
uint8 buf[512];
|
||||
FILE *io;
|
||||
|
||||
io = fopen(fname, "rb");
|
||||
if (!io) {
|
||||
die("Failed to open file for hashing");
|
||||
}
|
||||
|
||||
sha256_init(&sha256ctx);
|
||||
do {
|
||||
size_t br = fread(buf, 1, sizeof (buf), io);
|
||||
if (br > 0) {
|
||||
sha256_update(&sha256ctx, buf, br);
|
||||
}
|
||||
if (ferror(io)) {
|
||||
die("Error reading file for hashing");
|
||||
}
|
||||
} while (!feof(io));
|
||||
|
||||
fclose(io);
|
||||
|
||||
sha256_final(&sha256ctx, sha256);
|
||||
}
|
||||
|
||||
static int fileHashMatches(const char *fname, const unsigned char *wanted)
|
||||
{
|
||||
unsigned char sha256[32];
|
||||
hashFile(fname, sha256);
|
||||
return (memcmp(sha256, wanted, 32) == 0);
|
||||
}
|
||||
|
||||
static int fileNeedsUpdate(const ManifestItem *item)
|
||||
{
|
||||
if (item->len != fileLength(item->fname)) {
|
||||
infof("Update '%s', file size is different", item->fname);
|
||||
return 1; /* obviously different. */
|
||||
} else if (!fileHashMatches(item->fname, item->sha256)) {
|
||||
infof("Update '%s', file sha256 is different", item->fname);
|
||||
return 1;
|
||||
}
|
||||
|
||||
infof("Don't update '%s', the file is already up to date", item->fname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void downloadFile(const ManifestItem *item)
|
||||
{
|
||||
const char *outpath = "updates/downloads/";
|
||||
const size_t len = strlen(outpath) + strlen(item->fname) + 1;
|
||||
char *to = (char *) alloca(len);
|
||||
if (!to) {
|
||||
outOfMemory();
|
||||
}
|
||||
|
||||
snprintf(to, len, "%s%s", outpath, item->fname);
|
||||
|
||||
if ((item->len == fileLength(to)) && fileHashMatches(to, item->sha256)) {
|
||||
infof("Already downloaded '%s', not getting again", item->fname);
|
||||
} else {
|
||||
downloadURL(item->fname, to);
|
||||
if ((item->len != fileLength(to)) || !fileHashMatches(to, item->sha256)) {
|
||||
die("Download is incorrect or corrupted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int downloadUpdates(void)
|
||||
{
|
||||
int updatesAvailable = 0;
|
||||
ManifestItem *item;
|
||||
for (item = manifest; item != NULL; item = item->next) {
|
||||
item->update = fileNeedsUpdate(item);
|
||||
if (item->update) {
|
||||
updatesAvailable = 1;
|
||||
downloadFile(item);
|
||||
}
|
||||
}
|
||||
return updatesAvailable;
|
||||
}
|
||||
|
||||
static void maybeUpdateSelf(const char *argv0)
|
||||
{
|
||||
ManifestItem *item;
|
||||
|
||||
/* !!! FIXME: this needs to be a different string on macOS. */
|
||||
const char *fname = justFilename(argv0);
|
||||
|
||||
for (item = manifest; item != NULL; item = item->next) {
|
||||
if (strcasecmp(item->fname, fname) == 0) {
|
||||
if (fileNeedsUpdate(item)) {
|
||||
const char *outpath = "updates/downloads/";
|
||||
const size_t len = strlen(outpath) + strlen(item->fname) + 1;
|
||||
char *to = (char *) alloca(len);
|
||||
if (!to) {
|
||||
outOfMemory();
|
||||
}
|
||||
snprintf(to, len, "%s%s", outpath, item->fname);
|
||||
info("Have to upgrade the updater");
|
||||
downloadFile(item);
|
||||
chmod(to, 0777);
|
||||
|
||||
if (options.waitforprocess) {
|
||||
char pidstr[64];
|
||||
snprintf(pidstr, sizeof (pidstr), PIDFMT, (PIDFMTCAST) options.waitforprocess);
|
||||
launchProcess(to, to, "--updateself", argv0, "--waitpid", pidstr, NULL);
|
||||
} else {
|
||||
launchProcess(to, to, "--updateself", argv0, NULL);
|
||||
}
|
||||
die("Failed to initially launch upgraded updater");
|
||||
}
|
||||
break; /* done in any case. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void installUpdatedFile(const ManifestItem *item)
|
||||
{
|
||||
const char *basepath = "updates/downloads/";
|
||||
const size_t len = strlen(basepath) + strlen(item->fname) + 1;
|
||||
char *downloadPath = (char *) alloca(len);
|
||||
if (!downloadPath) {
|
||||
outOfMemory();
|
||||
}
|
||||
|
||||
snprintf(downloadPath, len, "%s%s", basepath, item->fname);
|
||||
|
||||
infof("Moving file for update: '%s' -> '%s'", downloadPath, item->fname);
|
||||
buildParentDirs(item->fname);
|
||||
if (rename(downloadPath, item->fname) == -1) {
|
||||
die("Failed to move updated file to final position");
|
||||
}
|
||||
}
|
||||
|
||||
static void applyUpdates(void)
|
||||
{
|
||||
FILE *io;
|
||||
ManifestItem *item;
|
||||
for (item = manifest; item != NULL; item = item->next) {
|
||||
if (!item->update) {
|
||||
continue;
|
||||
}
|
||||
|
||||
io = fopen(item->fname, "rb");
|
||||
if (io != NULL) {
|
||||
static int rollbackIndex = 0;
|
||||
char rollbackPath[64];
|
||||
fclose(io);
|
||||
item->rollback = ++rollbackIndex;
|
||||
snprintf(rollbackPath, sizeof (rollbackPath), "updates/rollbacks/%d", rollbackIndex);
|
||||
infof("Moving file for rollback: '%s' -> '%s'", item->fname, rollbackPath);
|
||||
remove(rollbackPath);
|
||||
if (rename(item->fname, rollbackPath) == -1) {
|
||||
die("failed to move to rollback dir");
|
||||
}
|
||||
}
|
||||
|
||||
installUpdatedFile(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void waitToApplyUpdates(void)
|
||||
{
|
||||
if (options.waitforprocess) {
|
||||
infof("Waiting for pid " PIDFMT " to die...", (PIDFMTCAST) options.waitforprocess);
|
||||
{
|
||||
#ifdef _WIN32
|
||||
windowsWaitForProcessToDie(options.waitforprocess);
|
||||
#else
|
||||
/* The parent opens a pipe on fd 3, and then forgets about it. We block
|
||||
on a read to that pipe here. When the game process quits (and the
|
||||
OS forcibly closes the pipe), we will unblock. Then we can loop on
|
||||
kill() until the process is truly gone. */
|
||||
int x = 0;
|
||||
read(3, &x, sizeof (x));
|
||||
info("Pipe has closed, waiting for process to fully go away now.");
|
||||
while (kill(options.waitforprocess, 0) == 0) {
|
||||
usleep(100000);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
info("pid is gone, continuing");
|
||||
}
|
||||
}
|
||||
|
||||
static void deleteRollbacks(void)
|
||||
{
|
||||
ManifestItem *item;
|
||||
for (item = manifest; item != NULL; item = item->next) {
|
||||
if (item->rollback) {
|
||||
char rollbackPath[64];
|
||||
snprintf(rollbackPath, sizeof (rollbackPath), "updates/rollbacks/%d", item->rollback);
|
||||
infof("delete rollback: %s", rollbackPath);
|
||||
remove(rollbackPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void chdirToBasePath(const char *argv0)
|
||||
{
|
||||
const char *fname = justFilename(argv0);
|
||||
size_t len;
|
||||
char *buf;
|
||||
|
||||
if (fname == argv0) { /* no path? Assume we're already there. */
|
||||
return;
|
||||
}
|
||||
|
||||
len = ((size_t) (fname - argv0)) - 1;
|
||||
buf = (char *) alloca(len);
|
||||
if (!buf) {
|
||||
outOfMemory();
|
||||
}
|
||||
|
||||
memcpy(buf, argv0, len);
|
||||
buf[len] = '\0';
|
||||
if (chdir(buf) == -1) {
|
||||
infof("base path is '%s'", buf);
|
||||
die("chdir to base path failed");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
#ifndef _WIN32
|
||||
signal(SIGPIPE, SIG_IGN); /* don't trigger signal when fd3 closes */
|
||||
#endif
|
||||
|
||||
logfile = stdout;
|
||||
chdirToBasePath(argv[0]);
|
||||
|
||||
makeDir("updates");
|
||||
makeDir("updates/downloads");
|
||||
makeDir("updates/rollbacks");
|
||||
|
||||
logfile = fopen("updates/updater-log.txt", "a");
|
||||
if (!logfile) {
|
||||
logfile = stdout;
|
||||
}
|
||||
|
||||
infof("Updater starting, %s", timestamp());
|
||||
|
||||
parseArgv(argc, argv);
|
||||
|
||||
/* if we have downloaded a new updater and restarted with that binary,
|
||||
replace the original updater and restart again in the right place. */
|
||||
if (options.updateself) {
|
||||
upgradeSelfAndRestart(argv[0]);
|
||||
}
|
||||
|
||||
prepHttpLib();
|
||||
|
||||
downloadManifest(); /* see if we need an update at all. */
|
||||
|
||||
maybeUpdateSelf(argv[0]); /* might relaunch if there's an updater upgrade. */
|
||||
|
||||
if (!downloadUpdates()) {
|
||||
info("Nothing needs updating, so we're done here!");
|
||||
} else {
|
||||
waitToApplyUpdates();
|
||||
applyUpdates();
|
||||
deleteRollbacks();
|
||||
info("You are now up to date!");
|
||||
}
|
||||
|
||||
freeManifest();
|
||||
shutdownHttpLib();
|
||||
|
||||
infof("Updater ending, %s", timestamp());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
156
code/autoupdater/sha256.c
Normal file
156
code/autoupdater/sha256.c
Normal file
|
@ -0,0 +1,156 @@
|
|||
/*********************************************************************
|
||||
* Filename: sha256.c
|
||||
* Author: Brad Conte (brad AT bradconte.com)
|
||||
* Copyright:
|
||||
* Disclaimer: This code is presented "as is" without any guarantees.
|
||||
* Details: Implementation of the SHA-256 hashing algorithm.
|
||||
SHA-256 is one of the three algorithms in the SHA2
|
||||
specification. The others, SHA-384 and SHA-512, are not
|
||||
offered in this implementation.
|
||||
Algorithm specification can be found here:
|
||||
* http://csrc.nist.gov/publications/fips/fips180-2/fips180-2withchangenotice.pdf
|
||||
This implementation uses little endian byte order.
|
||||
*********************************************************************/
|
||||
|
||||
/*************************** HEADER FILES ***************************/
|
||||
#include <stdlib.h>
|
||||
#include <memory.h>
|
||||
#include "sha256.h"
|
||||
|
||||
/****************************** MACROS ******************************/
|
||||
#define ROTLEFT(a,b) (((a) << (b)) | ((a) >> (32-(b))))
|
||||
#define ROTRIGHT(a,b) (((a) >> (b)) | ((a) << (32-(b))))
|
||||
|
||||
#define CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
|
||||
#define MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
|
||||
#define EP0(x) (ROTRIGHT(x,2) ^ ROTRIGHT(x,13) ^ ROTRIGHT(x,22))
|
||||
#define EP1(x) (ROTRIGHT(x,6) ^ ROTRIGHT(x,11) ^ ROTRIGHT(x,25))
|
||||
#define SIG0(x) (ROTRIGHT(x,7) ^ ROTRIGHT(x,18) ^ ((x) >> 3))
|
||||
#define SIG1(x) (ROTRIGHT(x,17) ^ ROTRIGHT(x,19) ^ ((x) >> 10))
|
||||
|
||||
/**************************** VARIABLES *****************************/
|
||||
static const uint32 k[64] = {
|
||||
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
||||
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
||||
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
||||
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
||||
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
||||
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
||||
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
||||
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
|
||||
};
|
||||
|
||||
/*********************** FUNCTION DEFINITIONS ***********************/
|
||||
void sha256_transform(SHA256_CTX *ctx, const uint8 data[])
|
||||
{
|
||||
uint32 a, b, c, d, e, f, g, h, i, j, t1, t2, m[64];
|
||||
|
||||
for (i = 0, j = 0; i < 16; ++i, j += 4)
|
||||
m[i] = (data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | (data[j + 3]);
|
||||
for ( ; i < 64; ++i)
|
||||
m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16];
|
||||
|
||||
a = ctx->state[0];
|
||||
b = ctx->state[1];
|
||||
c = ctx->state[2];
|
||||
d = ctx->state[3];
|
||||
e = ctx->state[4];
|
||||
f = ctx->state[5];
|
||||
g = ctx->state[6];
|
||||
h = ctx->state[7];
|
||||
|
||||
for (i = 0; i < 64; ++i) {
|
||||
t1 = h + EP1(e) + CH(e,f,g) + k[i] + m[i];
|
||||
t2 = EP0(a) + MAJ(a,b,c);
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + t1;
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = t1 + t2;
|
||||
}
|
||||
|
||||
ctx->state[0] += a;
|
||||
ctx->state[1] += b;
|
||||
ctx->state[2] += c;
|
||||
ctx->state[3] += d;
|
||||
ctx->state[4] += e;
|
||||
ctx->state[5] += f;
|
||||
ctx->state[6] += g;
|
||||
ctx->state[7] += h;
|
||||
}
|
||||
|
||||
void sha256_init(SHA256_CTX *ctx)
|
||||
{
|
||||
ctx->datalen = 0;
|
||||
ctx->bitlen = 0;
|
||||
ctx->state[0] = 0x6a09e667;
|
||||
ctx->state[1] = 0xbb67ae85;
|
||||
ctx->state[2] = 0x3c6ef372;
|
||||
ctx->state[3] = 0xa54ff53a;
|
||||
ctx->state[4] = 0x510e527f;
|
||||
ctx->state[5] = 0x9b05688c;
|
||||
ctx->state[6] = 0x1f83d9ab;
|
||||
ctx->state[7] = 0x5be0cd19;
|
||||
}
|
||||
|
||||
void sha256_update(SHA256_CTX *ctx, const uint8 data[], size_t len)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < len; ++i) {
|
||||
ctx->data[ctx->datalen] = data[i];
|
||||
ctx->datalen++;
|
||||
if (ctx->datalen == 64) {
|
||||
sha256_transform(ctx, ctx->data);
|
||||
ctx->bitlen += 512;
|
||||
ctx->datalen = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sha256_final(SHA256_CTX *ctx, uint8 hash[])
|
||||
{
|
||||
uint32 i = ctx->datalen;
|
||||
|
||||
// Pad whatever data is left in the buffer.
|
||||
if (ctx->datalen < 56) {
|
||||
ctx->data[i++] = 0x80;
|
||||
while (i < 56)
|
||||
ctx->data[i++] = 0x00;
|
||||
}
|
||||
else {
|
||||
ctx->data[i++] = 0x80;
|
||||
while (i < 64)
|
||||
ctx->data[i++] = 0x00;
|
||||
sha256_transform(ctx, ctx->data);
|
||||
memset(ctx->data, 0, 56);
|
||||
}
|
||||
|
||||
// Append to the padding the total message's length in bits and transform.
|
||||
ctx->bitlen += ctx->datalen * 8;
|
||||
ctx->data[63] = ctx->bitlen;
|
||||
ctx->data[62] = ctx->bitlen >> 8;
|
||||
ctx->data[61] = ctx->bitlen >> 16;
|
||||
ctx->data[60] = ctx->bitlen >> 24;
|
||||
ctx->data[59] = ctx->bitlen >> 32;
|
||||
ctx->data[58] = ctx->bitlen >> 40;
|
||||
ctx->data[57] = ctx->bitlen >> 48;
|
||||
ctx->data[56] = ctx->bitlen >> 56;
|
||||
sha256_transform(ctx, ctx->data);
|
||||
|
||||
// Since this implementation uses little endian byte ordering and SHA uses big endian,
|
||||
// reverse all the bytes when copying the final state to the output hash.
|
||||
for (i = 0; i < 4; ++i) {
|
||||
hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0x000000ff;
|
||||
hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0x000000ff;
|
||||
hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0x000000ff;
|
||||
hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0x000000ff;
|
||||
hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0x000000ff;
|
||||
hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0x000000ff;
|
||||
hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0x000000ff;
|
||||
hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0x000000ff;
|
||||
}
|
||||
}
|
42
code/autoupdater/sha256.h
Normal file
42
code/autoupdater/sha256.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*********************************************************************
|
||||
* Filename: sha256.h
|
||||
* Author: Brad Conte (brad AT bradconte.com)
|
||||
* Copyright:
|
||||
* Disclaimer: This code is presented "as is" without any guarantees.
|
||||
* Details: Defines the API for the corresponding SHA1 implementation.
|
||||
*********************************************************************/
|
||||
|
||||
#ifndef SHA256_H
|
||||
#define SHA256_H
|
||||
|
||||
/*************************** HEADER FILES ***************************/
|
||||
#include <stddef.h>
|
||||
|
||||
/****************************** MACROS ******************************/
|
||||
#define SHA256_BLOCK_SIZE 32 // SHA256 outputs a 32 byte digest
|
||||
|
||||
/**************************** DATA TYPES ****************************/
|
||||
#ifdef _MSC_VER
|
||||
typedef unsigned __int8 uint8;
|
||||
typedef unsigned __int32 uint32;
|
||||
typedef unsigned __int64 uint64;
|
||||
#else
|
||||
#include <stdint.h>
|
||||
typedef uint8_t uint8;
|
||||
typedef uint32_t uint32;
|
||||
typedef uint64_t uint64;
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint8 data[64];
|
||||
uint32 datalen;
|
||||
uint64 bitlen;
|
||||
uint32 state[8];
|
||||
} SHA256_CTX;
|
||||
|
||||
/*********************** FUNCTION DECLARATIONS **********************/
|
||||
void sha256_init(SHA256_CTX *ctx);
|
||||
void sha256_update(SHA256_CTX *ctx, const uint8 data[], size_t len);
|
||||
void sha256_final(SHA256_CTX *ctx, uint8 hash[]);
|
||||
|
||||
#endif // SHA256_H
|
86
code/sys/sys_autoupdater.c
Normal file
86
code/sys/sys_autoupdater.c
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
The code in this file is in the public domain. The rest of ioquake3
|
||||
is licensed under the GPLv2. Do not mingle code, please!
|
||||
*/
|
||||
|
||||
#ifdef USE_AUTOUPDATER
|
||||
# ifndef AUTOUPDATER_BIN
|
||||
# error The build system should have defined AUTOUPDATER_BIN
|
||||
# endif
|
||||
|
||||
# ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN 1
|
||||
# include <windows.h>
|
||||
# else
|
||||
# include <unistd.h>
|
||||
# endif
|
||||
|
||||
# include <stdio.h>
|
||||
# include <string.h>
|
||||
#endif
|
||||
|
||||
void Sys_LaunchAutoupdater(int argc, char **argv)
|
||||
{
|
||||
#ifdef USE_AUTOUPDATER
|
||||
#ifdef _WIN32
|
||||
{
|
||||
/* We don't need the Unix pipe() tapdance here because Windows lets children wait on parent processes. */
|
||||
PROCESS_INFORMATION procinfo;
|
||||
STARTUPINFO startinfo;
|
||||
char cmdline[128];
|
||||
memset(&procinfo, '\0', sizeof (procinfo));
|
||||
memset(&startinfo, '\0', sizeof (startinfo));
|
||||
startinfo.cb = sizeof (startinfo);
|
||||
sprintf(cmdline, "" AUTOUPDATER_BIN " --waitpid %u", (unsigned int) GetCurrentProcessId());
|
||||
|
||||
if (CreateProcessA(AUTOUPDATER_BIN, cmdline, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &startinfo, &procinfo))
|
||||
{
|
||||
/* close handles now so child cleans up immediately if nothing to do */
|
||||
CloseHandle(procinfo.hProcess);
|
||||
CloseHandle(procinfo.hThread);
|
||||
}
|
||||
}
|
||||
#else
|
||||
int updater_pipes[2];
|
||||
if (pipe(updater_pipes) == 0)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid == -1) /* failure, oh well. */
|
||||
{
|
||||
close(updater_pipes[0]);
|
||||
close(updater_pipes[1]);
|
||||
}
|
||||
else if (pid == 0) /* child process */
|
||||
{
|
||||
close(updater_pipes[1]); /* don't need write end. */
|
||||
if (dup2(updater_pipes[0], 3) != -1)
|
||||
{
|
||||
char pidstr[64];
|
||||
char *ptr = strrchr(argv[0], '/');
|
||||
if (ptr)
|
||||
*ptr = '\0';
|
||||
if (chdir(argv[0]) == -1) {
|
||||
_exit(1); /* oh well. */
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
if (chdir("../..") == -1) { /* put this at base of app bundle so paths make sense later. */
|
||||
_exit(1); /* oh well. */
|
||||
}
|
||||
#endif
|
||||
snprintf(pidstr, sizeof (pidstr), "%lld", (long long) getppid());
|
||||
execl(AUTOUPDATER_BIN, AUTOUPDATER_BIN, "--waitpid", pidstr, NULL);
|
||||
}
|
||||
_exit(0); /* oh well. */
|
||||
}
|
||||
else /* parent process */
|
||||
{
|
||||
/* leave the write end open until we terminate so updater can block on it. */
|
||||
close(updater_pipes[0]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
(void) argc; (void) argv; /* possibly unused. Pacify compilers. */
|
||||
}
|
||||
|
|
@ -683,6 +683,9 @@ int main( int argc, char **argv )
|
|||
int i;
|
||||
char commandLine[ MAX_STRING_CHARS ] = { 0 };
|
||||
|
||||
extern void Sys_LaunchAutoupdater(int argc, char **argv);
|
||||
Sys_LaunchAutoupdater(argc, argv);
|
||||
|
||||
#ifndef DEDICATED
|
||||
// SDL version check
|
||||
|
||||
|
|
Loading…
Reference in a new issue