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 /* no, this is wrong headed. make them supply a target if there is
200 * no default target provided in the bundle.
201 // a bogus default is provided if they don't specify the destination.
202 target = environment::get("TMP") + "/unbundled";
204 provided_target = false;
206 //LOG(astring("target is now ") + target);
207 environment::set(TARGET_WORD, target);
208 provided_target = true;
212 astring logdir = environment::get(LOGDIR_WORD);
215 logdir = environment::get("TMP") + "/logs";
216 environment::set(LOGDIR_WORD, logdir);
220 astring homedir = environment::get("HOME");
221 logdir = homedir + "/logs";
222 environment::set(LOGDIR_WORD, logdir);
227 astring keyword; // set if we were given a keyword on cmd line.
228 cmds.get_value("keyword", keyword);
230 astring vars_set; // we will document the variables we saw and show later.
232 for (int x = 0; x < cmds.entries(); x++) {
233 command_parameter curr = cmds.get(x);
234 if (curr.type() != command_parameter::VALUE) continue; // skip it.
235 if (curr.text().find('=', 0) < 0) continue; // no equals character.
236 variable_tokenizer t;
237 t.parse(curr.text());
238 if (!t.symbols()) continue; // didn't parse right.
239 astring var = t.table().name(0);
240 astring value = t.table()[0];
241 vars_set += astring("variable set: ") + var + "=" + value
242 + parser_bits::platform_eol_to_chars();
243 if (var == TARGET_WORD) {
244 provided_target = true;
246 //hmmm: handle LOGDIR passed as variable this way also!
247 _variables.add(var, value);
248 environment::set(var, value);
251 // get the most up to date version of the variable now.
252 astring logdir = environment::get(LOGDIR_WORD);
254 astring appname = filename(application_configuration::application_name()).rootname();
256 astring logname = logdir + "/" + appname + ".log";
257 // log_base *old_log = set_PW_logger_for_combo(logname);
258 standard_log_base *old_log = program_wide_logger::set(new combo_logger(logname));
261 BASE_LOG(astring('#', 76));
262 BASE_LOG(appname + " command-line parameters:");
263 BASE_LOG(cmds.text_form());
264 BASE_LOG(astring('#', 76));
269 // create a window so that installshield won't barf. this is only needed
270 // on windows when using this as a prerequisite for installshield.
271 window_handle f_window = create_simplistic_window("temp_stubby_class",
272 "stubby window title");
275 // read position for manifest offset out of our array.
276 byte_array temp_packed(2 * sizeof(int), MANIFEST_OFFSET_ARRAY + 8);
277 un_int manifest_offset;
278 if (!structures::obscure_detach(temp_packed, manifest_offset)) {
279 show_message(astring("could not read manifest offset in: ") + _global_argv[0],
284 filename this_exe(_global_argv[0]);
285 if (!this_exe.exists()) {
286 show_message(astring("could not access this exe image: ") + this_exe.raw(),
291 // start reading the manifest...
292 byte_filer our_exe(this_exe, "rb");
293 our_exe.seek(manifest_offset); // go to where the manifest starts.
295 // get number of chunks in manifest as the first bytes.
296 if (our_exe.read(temp_packed, 2 * sizeof(int)) <= 0) {
297 show_message(astring("could not read the manifest length in: ")
298 + this_exe.raw(), ERROR_TITLE);
302 structures::obscure_detach(temp_packed, item_count);
303 //check result of detach!
304 _manifest.insert(0, item_count); // add enough spaces for our item list.
306 // read each item definition out of the manifest now.
307 for (int i = 0; i < (int)item_count; i++) {
308 manifest_chunk &curr = _manifest[i];
309 bool worked = manifest_chunk::read_manifest(our_exe, curr);
313 curr.text_form(tmpork);
314 LOG(a_sprintf("item %d: ", i) + tmpork);
318 show_message(a_sprintf("could not read chunk for item #%d [%s]: ", i,
320 + this_exe.raw(), ERROR_TITLE);
326 LOG("read the following info from manifest:");
328 for (int i = 0; i < _manifest.length(); i++) {
329 manifest_chunk &curr = _manifest[i];
330 temp += a_sprintf("(%d) size %d, %s\n", i, curr._size,
333 critical_events::alert_message(temp, "manifest contents");
336 // now we should be just after the last byte of the manifest, at the
337 // first piece of data. we should read each chunk of data out and store
338 // it where it's supposed to go.
339 for (int festdex = 0; festdex < _manifest.length(); festdex++) {
340 manifest_chunk &curr = _manifest[festdex];
341 int size_left = curr._size;
343 // patch in our environment variables.
344 curr._payload = parser_bits::substitute_env_vars(curr._payload, false);
345 curr._parms = parser_bits::substitute_env_vars(curr._parms, false);
347 BASE_LOG(astring("processing ") + curr._payload
348 + a_sprintf(", size=%d, flags=%d", curr._size, curr._flags));
350 BASE_LOG(astring(" parms: ") + curr._parms);
353 // see if they specified a keyword on the command line.
354 bool keyword_good = true;
355 if (!!keyword && !curr._keywords.member(keyword)) {
356 // their keyword choice didn't match what we were pickled with.
357 keyword_good = false;
358 //BASE_LOG(astring("skipping ") + curr._payload + " for wrong keyword " + keyword);
361 if (curr._flags & SET_VARIABLE) {
363 // this is utterly different from a real target. we just set the
364 // variable and move on.
365 if (provided_target && (curr._payload == TARGET_WORD) ) {
366 BASE_LOG(astring("skipping ") + curr._payload + "=" + curr._parms
367 + ": was provided explicitly as " + target);
368 } else if (_variables.find(curr._payload)) {
369 BASE_LOG(astring("skipping ") + curr._payload + "=" + curr._parms
370 + ": was provided on command line.");
372 BASE_LOG(astring("setting ") + curr._payload + "=" + curr._parms);
373 environment::set(curr._payload, curr._parms);
375 // special code for changing logging directory midstream.
376 if (curr._payload == LOGDIR_WORD) {
377 astring logdir = curr._parms;
378 astring appname = filename(application_configuration::application_name()).rootname();
379 astring logname = logdir + "/" + appname + ".log";
380 standard_log_base *old_log = program_wide_logger::set(new combo_logger(logname));
381 /// log_base *old_log = set_PW_logger_for_combo(logname);
384 if (curr._payload == TARGET_WORD) {
385 // well we've now seen this defined.
386 provided_target = true;
391 } else if (curr._flags & TEST_VARIABLE_DEFINED) {
393 astring var_value = environment::get(curr._payload);
394 if (var_value.empty()) {
395 BASE_LOG(astring("assertion failed: ") + curr._payload + " is not defined!");
396 show_message(a_sprintf("failed test for variable %s: it is "
397 "*not* defined, for item #%d.", curr._payload.s(), festdex),
401 BASE_LOG(astring("assertion succeeded: ") + curr._payload + " defined as " + var_value);
406 if (! (curr._flags & OMIT_PACKING) ) {
407 // this one has a payload, so install it now if appropriate.
408 if (!provided_target) {
410 BASE_LOG(astring("No TARGET has been specified; please provide one on the command line.") + parser_bits::platform_eol_to_chars());
411 return print_instructions();
414 // make sure that the directories needed are present for the outputs.
415 filename target_dir = filename(curr._payload).dirname();
417 if (keyword_good && !target_dir.exists()
418 && !directory::recursive_create(target_dir)) {
419 LOG(a_sprintf("failed to create directory %s for item #%d: ",
420 target_dir.raw().s(), festdex) + curr._payload);
423 // test whether they wanted to allow overwriting.
424 if (curr._flags & NO_OVERWRITE) {
425 filename target_file(curr._payload);
426 if (target_file.exists()) {
427 BASE_LOG(astring("not overwriting existing ") + curr._payload);
428 keyword_good = false;
432 // see if this is supposed to be backed up before installation.
433 if (curr._flags & MAKE_BACKUP_FILE) {
434 filename target_file(curr._payload);
435 if (target_file.exists()) {
436 astring new_file_name = find_unique_backup_name(curr._payload);
437 if (!new_file_name) {
438 BASE_LOG(astring("failed to calculate new filename for ") + curr._payload);
439 keyword_good = false; // cancel the overwrite, couldn't backup.
441 // make a backup of the file by moving the old file name to the
442 // new file name, which should be unique, and then the current
443 // name is all clear to be written as a new file.
444 // BASE_LOG(astring("backing up ") + curr._payload + " --> " + new_file_name);
445 int retval = rename(curr._payload.s(), new_file_name.s());
447 BASE_LOG(astring("failed to rename ") + curr._payload + " as " + new_file_name);
448 keyword_good = false; // cancel the overwrite, couldn't backup.
454 byte_filer *targo = NIL;
455 if (keyword_good) targo = new byte_filer(curr._payload, "wb");
456 byte_array uncompressed(256 * KILOBYTE); // fluff it out to begin with.
457 byte_array temp(256 * KILOBYTE);
459 bool first_read = true;
460 // true if there haven't been any reads of the file before now.
461 bool too_tiny_complaint_already = false;
462 // becomes true if we complain about the file's size being larger than
463 // expected. this allows us to only complain once about each file.
465 // read a chunk at a time out of our exe image and store it into the
467 while (first_read || !our_exe.eof()) {
469 un_int real_size = 0, packed_size = 0;
470 // read in the real size from the file.
471 bool worked = manifest_chunk::read_an_obscured_int(our_exe, real_size);
473 show_message(a_sprintf("failed while reading real size "
474 "for item #%d: ", festdex) + curr._payload, ERROR_TITLE);
477 // read in the packed size now.
478 worked = manifest_chunk::read_an_obscured_int(our_exe, packed_size);
480 show_message(a_sprintf("failed while reading packed size "
481 "for item #%d: ", festdex) + curr._payload, ERROR_TITLE);
485 // make sure we don't eat the whole package--did the file end?
486 if ( (real_size == -1) && (packed_size == -1) ) {
487 // we've hit our sentinel; we've already unpacked all of this file.
492 BASE_LOG(a_sprintf("chunk packed_size=%d, real_size=%d", packed_size,
496 // now we know how big our next chunk is, so we can try reading it.
498 int ret = our_exe.read(temp, packed_size);
500 show_message(a_sprintf("failed while reading item #%d: ", festdex)
501 + curr._payload, ERROR_TITLE);
503 } else if (ret != packed_size) {
504 show_message(a_sprintf("bad trouble ahead, item #%d had different "
505 " size on read (expected %d, got %d): ", festdex, packed_size,
506 ret) + curr._payload, ERROR_TITLE);
509 uncompressed.reset(real_size + KILOBYTE); // add some for paranoia.
510 uLongf destlen = uncompressed.length();
511 int uncomp_ret = uncompress(uncompressed.access(), &destlen,
512 temp.observe(), packed_size);
513 if (uncomp_ret != Z_OK) {
514 show_message(a_sprintf("failed while uncompressing item #%d: ",
515 festdex) + curr._payload, ERROR_TITLE);
519 if (int(destlen) != real_size) {
520 LOG(a_sprintf("got a different unpacked size for item #%d: ",
521 festdex) + curr._payload);
524 // update the remaining size for this data chunk.
525 size_left -= real_size;
527 if (!too_tiny_complaint_already) {
528 LOG(a_sprintf("item #%d was larger than expected (non-fatal): ",
529 festdex) + curr._payload);
530 too_tiny_complaint_already = true;
533 // toss the extra bytes out.
534 uncompressed.zap(real_size, uncompressed.length() - 1);
537 // stuff the data we read into the target file.
538 ret = targo->write(uncompressed);
539 if (ret != uncompressed.length()) {
540 show_message(a_sprintf("failed while writing item #%d: ", festdex)
541 + curr._payload, ERROR_TITLE);
547 if (targo) targo->close();
549 // the file's written, but now we slap it's old time on it too.
551 if (!t.unpack(curr.c_filetime)) {
552 show_message(astring("failed to interpret timestamp for ")
553 + curr._payload, ERROR_TITLE);
556 // put the timestamp on the file.
557 t.set_time(curr._payload);
558 // utimbuf held_time;
559 // held_time.actime = t.raw();
560 // held_time.modtime = t.raw();
561 // // put the timestamp on the file.
562 // utime(curr._payload.s(), &held_time);
565 // now that we're pretty sure the file exists, we can run it if needed.
566 if ( (curr._flags & TARGET_EXECUTE) && keyword_good) {
567 // change the mode on the target file so we can execute it.
568 chmod(curr._payload.s(), 0766);
569 astring prev_dir = application_configuration::current_directory();
571 BASE_LOG(astring("launching ") + curr._payload);
573 BASE_LOG(astring(" with parameters: ") + curr._parms);
574 BASE_LOG(astring('-', 76));
577 basis::un_int retval = launch_process::run(curr._payload, curr._parms,
578 launch_process::AWAIT_APP_EXIT | launch_process::HIDE_APP_WINDOW, kid);
580 if (! (curr._flags & IGNORE_ERRORS) ) {
581 if (curr._flags & QUIET_FAILURE) {
582 // no message box for this, but still log it.
583 LOG(astring("failed to launch process, targ=")
584 + curr._payload + " with parms " + curr._parms
585 + a_sprintf(" error=%d", retval));
587 show_message(astring("failed to launch process, targ=")
588 + curr._payload + " with parms " + curr._parms
589 + a_sprintf(" error=%d", retval), ERROR_TITLE);
591 return retval; // pass along same exit value we were told.
593 LOG(astring("ignoring failure to launch process, targ=")
594 + curr._payload + " with parms " + curr._parms
595 + a_sprintf(" error=%d", retval));
599 chdir(prev_dir.s()); // reset directory pointer, just in case.
601 BASE_LOG(astring('-', 76));
607 whack_simplistic_window(f_window);
613 ////////////////////////////////////////////////////////////////////////////
615 HOOPLE_MAIN(unpacker_stub, )
617 #ifdef __BUILD_STATIC_APPLICATION__
618 // static dependencies found by buildor_gen_deps.sh:
619 #include <application/application_shell.cpp>
620 #include <application/command_line.cpp>
621 #include <basis/astring.cpp>
622 #include <basis/common_outcomes.cpp>
623 #include <basis/environment.cpp>
624 #include <basis/mutex.cpp>
625 #include <basis/utf_conversion.cpp>
626 #include <configuration/application_configuration.cpp>
627 #include <configuration/configurator.cpp>
628 #include <configuration/ini_configurator.cpp>
629 #include <configuration/ini_parser.cpp>
630 #include <configuration/table_configurator.cpp>
631 #include <configuration/variable_tokenizer.cpp>
632 #include <filesystem/byte_filer.cpp>
633 #include <filesystem/directory.cpp>
634 #include <filesystem/file_info.cpp>
635 #include <filesystem/filename.cpp>
636 #include <filesystem/filename_list.cpp>
637 #include <filesystem/file_time.cpp>
638 #include <filesystem/heavy_file_ops.cpp>
639 #include <filesystem/huge_file.cpp>
640 #include <loggers/combo_logger.cpp>
641 #include <loggers/console_logger.cpp>
642 #include <loggers/critical_events.cpp>
643 #include <loggers/file_logger.cpp>
644 #include <loggers/program_wide_logger.cpp>
645 #include <processes/launch_process.cpp>
646 #include <structures/bit_vector.cpp>
647 #include <structures/checksums.cpp>
648 #include <structures/object_packers.cpp>
649 #include <structures/static_memory_gremlin.cpp>
650 #include <structures/string_hasher.cpp>
651 #include <structures/string_table.cpp>
652 #include <structures/version_record.cpp>
653 #include <textual/byte_formatter.cpp>
654 #include <textual/parser_bits.cpp>
655 #include <textual/string_manipulation.cpp>
656 #include <timely/earth_time.cpp>
657 #include <timely/time_control.cpp>
658 #include <timely/time_stamp.cpp>
659 #endif // __BUILD_STATIC_APPLICATION__