1 /*****************************************************************************\
3 * Name : unpacker stub program *
4 * Author : Chris Koeritz *
6 *******************************************************************************
7 * Copyright (c) 2006-$now By Author. This program is free software; you can *
8 * redistribute it and/or modify it under the terms of the GNU General Public *
9 * License as published by the Free Software Foundation; either version 2 of *
10 * the License or (at your option) any later version. This is online at: *
11 * http://www.fsf.org/copyleft/gpl.html *
12 * Please send any updates to: fred@gruntose.com *
13 \*****************************************************************************/
15 #include "common_bundle.h"
17 #include <application/command_line.h>
18 #include <application/hoople_main.h>
19 #include <application/window_classist.h>
20 #include <basis/array.h>
21 #include <basis/byte_array.h>
22 #include <basis/environment.h>
23 #include <basis/guards.h>
24 #include <basis/utf_conversion.h>
25 #include <configuration/application_configuration.h>
26 #include <configuration/variable_tokenizer.h>
27 #include <filesystem/byte_filer.h>
28 #include <filesystem/directory.h>
29 #include <filesystem/filename.h>
30 #include <filesystem/file_time.h>
31 #include <filesystem/heavy_file_ops.h>
32 #include <loggers/console_logger.h>
33 #include <loggers/critical_events.h>
34 #include <loggers/file_logger.h>
35 #include <processes/launch_process.h>
36 #include <structures/static_memory_gremlin.h>
37 #include <structures/string_table.h>
38 #include <textual/parser_bits.h>
50 #include <sys/utime.h>
53 using namespace application;
54 using namespace basis;
55 using namespace configuration;
56 using namespace filesystem;
57 using namespace loggers;
58 using namespace processes;
59 using namespace structures;
60 using namespace textual;
62 const int CHUNKING_SIZE = 64 * KILOBYTE;
63 // we'll read this big a chunk from a source file at a time.
65 const astring TARGET_WORD = "TARGET";
66 const astring LOGDIR_WORD = "LOGDIR";
68 #define BASE_LOG(to_print) program_wide_logger::get().log(to_print, ALWAYS_PRINT)
69 #define LOG(to_print) STAMPED_EMERGENCY_LOG(program_wide_logger::get(), to_print)
72 // uncomment for noisier version.
74 const char *ERROR_TITLE = "An Error Caused Incomplete Installation";
75 // used in error messages as the title.
77 ////////////////////////////////////////////////////////////////////////////
79 class unpacker_stub : public application_shell
82 unpacker_stub() : application_shell(), _app_name(filename(_global_argv[0]).basename()) {}
83 DEFINE_CLASS_NAME("unpacker_stub");
85 int print_instructions();
87 virtual int execute();
91 array<manifest_chunk> _manifest; //!< the list of chunks to unpack.
92 string_table _variables; //!< our list of variable overrides.
95 ////////////////////////////////////////////////////////////////////////////
97 void show_message(const astring &msg, const astring &title)
101 BASE_LOG(astring('-', title.length()));
104 MessageBox(0, to_unicode_temp(msg), to_unicode_temp(title),
105 MB_OK | MB_ICONINFORMATION);
109 int unpacker_stub::print_instructions()
112 %s: This program unpacks its contents into the locations\n\
113 specified at packing time. The --target flag can be used to specify a\n\
114 different %s directory for the installation (for components that use\n\
115 the default %s variable to specify their install folder).\n\
116 One can also pass a --keyword flag to specify a keyword; the files in the\n\
117 bundle that are marked with that keyword will be installed, but files that\n\
118 are missing the keyword will not be.\n\
119 Further, variables can be overridden on the command line in the\n\
121 The line below uses all these parameters as an example:\n\n\
122 %s --target c:\\Program Files\\gubernator --keyword dlls_only SILENT=true\n\
126 One helpful variable is \"%s\". This is where the unpacking log file\n\
127 will be written to. The default is \"~/logs\" (or \"$TMP/logs\" on win32)\n\
129 \n", _app_name.s(), TARGET_WORD.s(), TARGET_WORD.s(), _app_name.s(), LOGDIR_WORD.s());
130 show_message(msg, "Unpacking Instructions");
134 // creates a unique backup file name, if it can.
135 // we assume that this file already exists, but we want to check for
136 // our backup file naming scheme in case we already backed this up
137 // some previous time.
138 astring find_unique_backup_name(astring original_file)
140 const int MAXIMUM_BACKUPS = 200;
142 for (int i = 0; i < MAXIMUM_BACKUPS; i++) {
143 filename target_file = original_file + a_sprintf(".%04d", i);
144 if (target_file.exists()) {
145 //BASE_LOG(astring("bkup already here: ") + target_file);
148 // this file is okay to use.
151 return ""; // nothing found.
155 // the string embedded into the array is not mentioned anywhere else in this
156 // program, which should allow the packer to find it and fix the manifest
157 // size. the keyword is: "muftiloc", and the first bytes that can be
158 // overwritten are its beginning offset plus its length of 8 chars. there
159 // is room for 8 bytes after the tag, but currently the first 4 are used as
161 abyte MANIFEST_OFFSET_ARRAY[]
162 = { 'm', 'u', 'f', 't', 'i', 'l', 'o', 'c', 0, 0, 0, 0, 0, 0, 0, 0 };
164 int unpacker_stub::execute()
168 if (IsUserAnAdmin()) {
169 ::MessageBox(0, to_unicode_temp("IS admin in bundler"), to_unicode_temp("bundler"), MB_OK);
171 ::MessageBox(0, to_unicode_temp("NOT admin in bundler"), to_unicode_temp("bundler"), MB_OK);
176 command_line cmds(_global_argc, _global_argv);
179 if (cmds.find('?', indy)) return print_instructions();
180 if (cmds.find("?", indy)) return print_instructions();
182 // make sure we provide the same services as the bundle creator for the
183 // default set of variables.
185 environment::set("EXE_END", ""); // executable file ending.
186 environment::set("DLL_START", "lib"); // dll file prefix.
187 environment::set("DLL_END", ".so"); // dll file ending.
189 environment::set("EXE_END", ".exe");
190 environment::set("DLL_START", "");
191 environment::set("DLL_END", ".dll");
194 // set TARGET directory if passed on the command line,
195 bool provided_target = false; // true if the command line specified target.
197 cmds.get_value("target", target);
199 provided_target = false;
201 //LOG(astring("target is now ") + target);
202 environment::set(TARGET_WORD, target);
203 provided_target = true;
207 astring logdir = environment::get(LOGDIR_WORD);
210 logdir = environment::TMP() + "/logs";
211 environment::set(LOGDIR_WORD, logdir);
215 astring homedir = environment::get("HOME");
216 logdir = homedir + "/logs";
217 environment::set(LOGDIR_WORD, logdir);
222 astring keyword; // set if we were given a keyword on cmd line.
223 cmds.get_value("keyword", keyword);
225 astring vars_set; // we will document the variables we saw and show later.
227 for (int x = 0; x < cmds.entries(); x++) {
228 command_parameter curr = cmds.get(x);
229 if (curr.type() != command_parameter::VALUE) continue; // skip it.
230 if (curr.text().find('=', 0) < 0) continue; // no equals character.
231 variable_tokenizer t;
232 t.parse(curr.text());
233 if (!t.symbols()) continue; // didn't parse right.
234 astring var = t.table().name(0);
235 astring value = t.table()[0];
236 vars_set += astring("variable set: ") + var + "=" + value
237 + parser_bits::platform_eol_to_chars();
238 if (var == TARGET_WORD) {
239 provided_target = true;
241 //hmmm: handle LOGDIR passed as variable this way also!
242 _variables.add(var, value);
243 environment::set(var, value);
246 // get the most up to date version of the variable now.
247 astring logdir = environment::get(LOGDIR_WORD);
249 astring appname = filename(application_configuration::application_name()).rootname();
251 astring logname = logdir + "/" + appname + ".log";
252 // log_base *old_log = set_PW_logger_for_combo(logname);
253 standard_log_base *old_log = program_wide_logger::set(new combo_logger(logname));
256 BASE_LOG(astring('#', 76));
257 BASE_LOG(appname + " command-line parameters:");
258 BASE_LOG(cmds.text_form());
259 BASE_LOG(astring('#', 76));
264 // create a window so that installshield won't barf. this is only needed
265 // on windows when using this as a prerequisite for installshield.
266 window_handle f_window = create_simplistic_window("temp_stubby_class",
267 "stubby window title");
270 // read position for manifest offset out of our array.
271 byte_array temp_packed(2 * sizeof(int), MANIFEST_OFFSET_ARRAY + 8);
272 un_int manifest_offset;
273 if (!structures::obscure_detach(temp_packed, manifest_offset)) {
274 show_message(astring("could not read manifest offset in: ") + _global_argv[0],
279 filename this_exe(_global_argv[0]);
280 if (!this_exe.exists()) {
281 show_message(astring("could not access this exe image: ") + this_exe.raw(),
286 // start reading the manifest...
287 byte_filer our_exe(this_exe, "rb");
288 our_exe.seek(manifest_offset); // go to where the manifest starts.
290 // get number of chunks in manifest as the first bytes.
291 if (our_exe.read(temp_packed, 2 * sizeof(int)) <= 0) {
292 show_message(astring("could not read the manifest length in: ")
293 + this_exe.raw(), ERROR_TITLE);
297 structures::obscure_detach(temp_packed, item_count);
298 //check result of detach!
299 _manifest.insert(0, item_count); // add enough spaces for our item list.
301 // read each item definition out of the manifest now.
302 for (int i = 0; i < (int)item_count; i++) {
303 manifest_chunk &curr = _manifest[i];
304 bool worked = manifest_chunk::read_manifest(our_exe, curr);
308 curr.text_form(tmpork);
309 LOG(a_sprintf("item %d: ", i) + tmpork);
313 show_message(a_sprintf("could not read chunk for item #%d [%s]: ", i,
315 + this_exe.raw(), ERROR_TITLE);
321 LOG("read the following info from manifest:");
323 for (int i = 0; i < _manifest.length(); i++) {
324 manifest_chunk &curr = _manifest[i];
325 temp += a_sprintf("(%d) size %d, %s\n", i, curr._size,
328 critical_events::alert_message(temp, "manifest contents");
331 // now we should be just after the last byte of the manifest, at the
332 // first piece of data. we should read each chunk of data out and store
333 // it where it's supposed to go.
334 for (int festdex = 0; festdex < _manifest.length(); festdex++) {
335 manifest_chunk &curr = _manifest[festdex];
336 int size_left = curr._size;
338 // patch in our environment variables.
339 curr._payload = parser_bits::substitute_env_vars(curr._payload, false);
340 curr._parms = parser_bits::substitute_env_vars(curr._parms, false);
342 BASE_LOG(astring("processing ") + curr._payload
343 + a_sprintf(", size=%d, flags=%d", curr._size, curr._flags));
345 BASE_LOG(astring(" parms: ") + curr._parms);
348 // see if they specified a keyword on the command line.
349 bool keyword_good = true;
350 if (!!keyword && !curr._keywords.member(keyword)) {
351 // their keyword choice didn't match what we were pickled with.
352 keyword_good = false;
353 //BASE_LOG(astring("skipping ") + curr._payload + " for wrong keyword " + keyword);
356 if (curr._flags & SET_VARIABLE) {
358 // this is utterly different from a real target. we just set the
359 // variable and move on.
360 if (provided_target && (curr._payload == TARGET_WORD) ) {
361 BASE_LOG(astring("skipping ") + curr._payload + "=" + curr._parms
362 + ": was provided explicitly as " + target);
363 } else if (_variables.find(curr._payload)) {
364 BASE_LOG(astring("skipping ") + curr._payload + "=" + curr._parms
365 + ": was provided on command line.");
367 BASE_LOG(astring("setting ") + curr._payload + "=" + curr._parms);
368 environment::set(curr._payload, curr._parms);
370 // special code for changing logging directory midstream.
371 if (curr._payload == LOGDIR_WORD) {
372 astring logdir = curr._parms;
373 astring appname = filename(application_configuration::application_name()).rootname();
374 astring logname = logdir + "/" + appname + ".log";
375 standard_log_base *old_log = program_wide_logger::set(new combo_logger(logname));
376 /// log_base *old_log = set_PW_logger_for_combo(logname);
379 if (curr._payload == TARGET_WORD) {
380 // well we've now seen this defined.
381 provided_target = true;
386 } else if (curr._flags & TEST_VARIABLE_DEFINED) {
388 astring var_value = environment::get(curr._payload);
389 if (var_value.empty()) {
390 BASE_LOG(astring("assertion failed: ") + curr._payload + " is not defined!");
391 show_message(a_sprintf("failed test for variable %s: it is "
392 "*not* defined, for item #%d.", curr._payload.s(), festdex),
396 BASE_LOG(astring("assertion succeeded: ") + curr._payload + " defined as " + var_value);
401 if (! (curr._flags & OMIT_PACKING) ) {
402 // this one has a payload, so install it now if appropriate.
403 if (!provided_target) {
405 BASE_LOG(astring("No TARGET has been specified; please provide one on the command line.") + parser_bits::platform_eol_to_chars());
406 return print_instructions();
409 // make sure that the directories needed are present for the outputs.
410 filename target_dir = filename(curr._payload).dirname();
412 if (keyword_good && !target_dir.exists()
413 && !directory::recursive_create(target_dir)) {
414 LOG(a_sprintf("failed to create directory %s for item #%d: ",
415 target_dir.raw().s(), festdex) + curr._payload);
418 // test whether they wanted to allow overwriting.
419 if (curr._flags & NO_OVERWRITE) {
420 filename target_file(curr._payload);
421 if (target_file.exists()) {
422 BASE_LOG(astring("not overwriting existing ") + curr._payload);
423 keyword_good = false;
427 // see if this is supposed to be backed up before installation.
428 if (curr._flags & MAKE_BACKUP_FILE) {
429 filename target_file(curr._payload);
430 if (target_file.exists()) {
431 astring new_file_name = find_unique_backup_name(curr._payload);
432 if (!new_file_name) {
433 BASE_LOG(astring("failed to calculate new filename for ") + curr._payload);
434 keyword_good = false; // cancel the overwrite, couldn't backup.
436 // make a backup of the file by moving the old file name to the
437 // new file name, which should be unique, and then the current
438 // name is all clear to be written as a new file.
439 // BASE_LOG(astring("backing up ") + curr._payload + " --> " + new_file_name);
440 int retval = rename(curr._payload.s(), new_file_name.s());
442 BASE_LOG(astring("failed to rename ") + curr._payload + " as " + new_file_name);
443 keyword_good = false; // cancel the overwrite, couldn't backup.
449 byte_filer *targo = NULL_POINTER;
450 if (keyword_good) targo = new byte_filer(curr._payload, "wb");
451 byte_array uncompressed(256 * KILOBYTE); // fluff it out to begin with.
452 byte_array temp(256 * KILOBYTE);
454 bool first_read = true;
455 // true if there haven't been any reads of the file before now.
456 bool too_tiny_complaint_already = false;
457 // becomes true if we complain about the file's size being larger than
458 // expected. this allows us to only complain once about each file.
460 // read a chunk at a time out of our exe image and store it into the
462 while (first_read || !our_exe.eof()) {
464 un_int real_size = 0, packed_size = 0;
465 // read in the real size from the file.
466 bool worked = manifest_chunk::read_an_obscured_int(our_exe, real_size);
468 show_message(a_sprintf("failed while reading real size "
469 "for item #%d: ", festdex) + curr._payload, ERROR_TITLE);
472 // read in the packed size now.
473 worked = manifest_chunk::read_an_obscured_int(our_exe, packed_size);
475 show_message(a_sprintf("failed while reading packed size "
476 "for item #%d: ", festdex) + curr._payload, ERROR_TITLE);
480 // make sure we don't eat the whole package--did the file end?
481 if ( (real_size == -1) && (packed_size == -1) ) {
482 // we've hit our sentinel; we've already unpacked all of this file.
487 BASE_LOG(a_sprintf("chunk packed_size=%d, real_size=%d", packed_size,
491 // now we know how big our next chunk is, so we can try reading it.
493 int ret = our_exe.read(temp, packed_size);
495 show_message(a_sprintf("failed while reading item #%d: ", festdex)
496 + curr._payload, ERROR_TITLE);
498 } else if (ret != packed_size) {
499 show_message(a_sprintf("bad trouble ahead, item #%d had different "
500 " size on read (expected %d, got %d): ", festdex, packed_size,
501 ret) + curr._payload, ERROR_TITLE);
504 uncompressed.reset(real_size + KILOBYTE); // add some for paranoia.
505 uLongf destlen = uncompressed.length();
506 int uncomp_ret = uncompress(uncompressed.access(), &destlen,
507 temp.observe(), packed_size);
508 if (uncomp_ret != Z_OK) {
509 show_message(a_sprintf("failed while uncompressing item #%d: ",
510 festdex) + curr._payload, ERROR_TITLE);
514 if (int(destlen) != real_size) {
515 LOG(a_sprintf("got a different unpacked size for item #%d: ",
516 festdex) + curr._payload);
519 // update the remaining size for this data chunk.
520 size_left -= real_size;
522 if (!too_tiny_complaint_already) {
523 LOG(a_sprintf("item #%d was larger than expected (non-fatal): ",
524 festdex) + curr._payload);
525 too_tiny_complaint_already = true;
528 // toss the extra bytes out.
529 uncompressed.zap(real_size, uncompressed.length() - 1);
532 // stuff the data we read into the target file.
533 ret = targo->write(uncompressed);
534 if (ret != uncompressed.length()) {
535 show_message(a_sprintf("failed while writing item #%d: ", festdex)
536 + curr._payload, ERROR_TITLE);
542 if (targo) targo->close();
544 // the file's written, but now we slap it's old time on it too.
546 if (!t.unpack(curr.c_filetime)) {
547 show_message(astring("failed to interpret timestamp for ")
548 + curr._payload, ERROR_TITLE);
551 // put the timestamp on the file.
552 t.set_time(curr._payload);
553 // utimbuf held_time;
554 // held_time.actime = t.raw();
555 // held_time.modtime = t.raw();
556 // // put the timestamp on the file.
557 // utime(curr._payload.s(), &held_time);
560 // now that we're pretty sure the file exists, we can run it if needed.
561 if ( (curr._flags & TARGET_EXECUTE) && keyword_good) {
562 // change the mode on the target file so we can execute it.
563 chmod(curr._payload.s(), 0766);
564 astring prev_dir = application_configuration::current_directory();
566 BASE_LOG(astring("launching ") + curr._payload);
568 BASE_LOG(astring(" with parameters: ") + curr._parms);
569 BASE_LOG(astring('-', 76));
572 basis::un_int retval = launch_process::run(curr._payload, curr._parms,
573 launch_process::AWAIT_APP_EXIT | launch_process::HIDE_APP_WINDOW, kid);
575 if (! (curr._flags & IGNORE_ERRORS) ) {
576 if (curr._flags & QUIET_FAILURE) {
577 // no message box for this, but still log it.
578 LOG(astring("failed to launch process, targ=")
579 + curr._payload + " with parms " + curr._parms
580 + a_sprintf(" error=%d", retval));
582 show_message(astring("failed to launch process, targ=")
583 + curr._payload + " with parms " + curr._parms
584 + a_sprintf(" error=%d", retval), ERROR_TITLE);
586 return retval; // pass along same exit value we were told.
588 LOG(astring("ignoring failure to launch process, targ=")
589 + curr._payload + " with parms " + curr._parms
590 + a_sprintf(" error=%d", retval));
594 chdir(prev_dir.s()); // reset directory pointer, just in case.
596 BASE_LOG(astring('-', 76));
602 whack_simplistic_window(f_window);
608 ////////////////////////////////////////////////////////////////////////////
610 HOOPLE_MAIN(unpacker_stub, )
612 #ifdef __BUILD_STATIC_APPLICATION__
613 // static dependencies found by buildor_gen_deps.sh:
614 #include <application/application_shell.cpp>
615 #include <application/command_line.cpp>
616 #include <basis/astring.cpp>
617 #include <basis/common_outcomes.cpp>
618 #include <basis/environment.cpp>
619 #include <basis/mutex.cpp>
620 #include <basis/utf_conversion.cpp>
621 #include <configuration/application_configuration.cpp>
622 #include <configuration/configurator.cpp>
623 #include <configuration/ini_configurator.cpp>
624 #include <configuration/ini_parser.cpp>
625 #include <configuration/table_configurator.cpp>
626 #include <configuration/variable_tokenizer.cpp>
627 #include <filesystem/byte_filer.cpp>
628 #include <filesystem/directory.cpp>
629 #include <filesystem/file_info.cpp>
630 #include <filesystem/filename.cpp>
631 #include <filesystem/filename_list.cpp>
632 #include <filesystem/file_time.cpp>
633 #include <filesystem/heavy_file_ops.cpp>
634 #include <filesystem/huge_file.cpp>
635 #include <loggers/combo_logger.cpp>
636 #include <loggers/console_logger.cpp>
637 #include <loggers/critical_events.cpp>
638 #include <loggers/file_logger.cpp>
639 #include <loggers/program_wide_logger.cpp>
640 #include <processes/launch_process.cpp>
641 #include <structures/bit_vector.cpp>
642 #include <structures/checksums.cpp>
643 #include <structures/object_packers.cpp>
644 #include <structures/static_memory_gremlin.cpp>
645 #include <structures/string_hasher.cpp>
646 #include <structures/string_table.cpp>
647 #include <structures/version_record.cpp>
648 #include <textual/byte_formatter.cpp>
649 #include <textual/parser_bits.cpp>
650 #include <textual/string_manipulation.cpp>
651 #include <timely/earth_time.cpp>
652 #include <timely/time_control.cpp>
653 #include <timely/time_stamp.cpp>
654 #endif // __BUILD_STATIC_APPLICATION__