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>
51 #include <sys/utime.h>
55 using namespace application;
56 using namespace basis;
57 using namespace configuration;
58 using namespace filesystem;
59 using namespace loggers;
60 using namespace processes;
61 using namespace structures;
62 using namespace textual;
64 const int CHUNKING_SIZE = 64 * KILOBYTE;
65 // we'll read this big a chunk from a source file at a time.
67 const astring TARGET_WORD = "TARGET";
68 const astring LOGDIR_WORD = "LOGDIR";
70 #define BASE_LOG(to_print) program_wide_logger::get().log(to_print, ALWAYS_PRINT)
71 #define LOG(to_print) STAMPED_EMERGENCY_LOG(program_wide_logger::get(), to_print)
74 // uncomment for noisier version.
76 const char *ERROR_TITLE = "An Error Caused Incomplete Installation";
77 // used in error messages as the title.
79 ////////////////////////////////////////////////////////////////////////////
81 class unpacker_stub : public application_shell
84 unpacker_stub() : application_shell(), _app_name(filename(_global_argv[0]).basename()) {}
85 DEFINE_CLASS_NAME("unpacker_stub");
87 int print_instructions();
89 virtual int execute();
93 array<manifest_chunk> _manifest; //!< the list of chunks to unpack.
94 string_table _variables; //!< our list of variable overrides.
97 ////////////////////////////////////////////////////////////////////////////
99 void show_message(const astring &msg, const astring &title)
103 BASE_LOG(astring('-', title.length()));
106 MessageBox(0, to_unicode_temp(msg), to_unicode_temp(title),
107 MB_OK | MB_ICONINFORMATION);
111 int unpacker_stub::print_instructions()
114 %s: This program unpacks its contents into the locations\n\
115 specified at packing time. The --target flag can be used to specify a\n\
116 different %s directory for the installation (for components that use\n\
117 the default %s variable to specify their install folder).\n\
118 One can also pass a --keyword flag to specify a keyword; the files in the\n\
119 bundle that are marked with that keyword will be installed, but files that\n\
120 are missing the keyword will not be.\n\
121 Further, variables can be overridden on the command line in the\n\
123 The line below uses all these parameters as an example:\n\n\
124 %s --target c:\\Program Files\\gubernator --keyword dlls_only SILENT=true\n\
128 One helpful variable is \"%s\". This is where the unpacking log file\n\
129 will be written to. The default is \"~/logs\" (or \"$TMP/logs\" on win32)\n\
131 \n", _app_name.s(), TARGET_WORD.s(), TARGET_WORD.s(), _app_name.s(), LOGDIR_WORD.s());
132 show_message(msg, "Unpacking Instructions");
136 // creates a unique backup file name, if it can.
137 // we assume that this file already exists, but we want to check for
138 // our backup file naming scheme in case we already backed this up
139 // some previous time.
140 astring find_unique_backup_name(astring original_file)
142 const int MAXIMUM_BACKUPS = 200;
144 for (int i = 0; i < MAXIMUM_BACKUPS; i++) {
145 filename target_file = original_file + a_sprintf(".%04d", i);
146 if (target_file.exists()) {
147 //BASE_LOG(astring("bkup already here: ") + target_file);
150 // this file is okay to use.
153 return ""; // nothing found.
157 // the string embedded into the array is not mentioned anywhere else in this
158 // program, which should allow the packer to find it and fix the manifest
159 // size. the keyword is: "muftiloc", and the first bytes that can be
160 // overwritten are its beginning offset plus its length of 8 chars. there
161 // is room for 8 bytes after the tag, but currently the first 4 are used as
163 abyte MANIFEST_OFFSET_ARRAY[]
164 = { 'm', 'u', 'f', 't', 'i', 'l', 'o', 'c', 0, 0, 0, 0, 0, 0, 0, 0 };
166 int unpacker_stub::execute()
170 if (IsUserAnAdmin()) {
171 ::MessageBox(0, to_unicode_temp("IS admin in bundler"), to_unicode_temp("bundler"), MB_OK);
173 ::MessageBox(0, to_unicode_temp("NOT admin in bundler"), to_unicode_temp("bundler"), MB_OK);
178 command_line cmds(_global_argc, _global_argv);
181 if (cmds.find('?', indy)) return print_instructions();
182 if (cmds.find("?", indy)) return print_instructions();
184 // make sure we provide the same services as the bundle creator for the
185 // default set of variables.
187 environment::set("EXE_END", ""); // executable file ending.
188 environment::set("DLL_START", "lib"); // dll file prefix.
189 environment::set("DLL_END", ".so"); // dll file ending.
191 environment::set("EXE_END", ".exe");
192 environment::set("DLL_START", "");
193 environment::set("DLL_END", ".dll");
196 // set TARGET directory if passed on the command line,
197 bool provided_target = false; // true if the command line specified target.
199 cmds.get_value("target", target);
201 provided_target = false;
203 //LOG(astring("target is now ") + target);
204 environment::set(TARGET_WORD, target);
205 provided_target = true;
209 astring logdir = environment::get(LOGDIR_WORD);
212 logdir = environment::TMP() + "/logs";
213 environment::set(LOGDIR_WORD, logdir);
217 astring homedir = environment::get("HOME");
218 logdir = homedir + "/logs";
219 environment::set(LOGDIR_WORD, logdir);
224 astring keyword; // set if we were given a keyword on cmd line.
225 cmds.get_value("keyword", keyword);
227 astring vars_set; // we will document the variables we saw and show later.
229 for (int x = 0; x < cmds.entries(); x++) {
230 command_parameter curr = cmds.get(x);
231 if (curr.type() != command_parameter::VALUE) continue; // skip it.
232 if (curr.text().find('=', 0) < 0) continue; // no equals character.
233 variable_tokenizer t;
234 t.parse(curr.text());
235 if (!t.symbols()) continue; // didn't parse right.
236 astring var = t.table().name(0);
237 astring value = t.table()[0];
238 vars_set += astring("variable set: ") + var + "=" + value
239 + parser_bits::platform_eol_to_chars();
240 if (var == TARGET_WORD) {
241 provided_target = true;
243 //hmmm: handle LOGDIR passed as variable this way also!
244 _variables.add(var, value);
245 environment::set(var, value);
248 // get the most up to date version of the variable now.
249 astring logdir = environment::get(LOGDIR_WORD);
251 astring appname = filename(application_configuration::application_name()).rootname();
253 astring logname = logdir + "/" + appname + ".log";
254 // log_base *old_log = set_PW_logger_for_combo(logname);
255 standard_log_base *old_log = program_wide_logger::set(new combo_logger(logname));
258 BASE_LOG(astring('#', 76));
259 BASE_LOG(appname + " command-line parameters:");
260 BASE_LOG(cmds.text_form());
261 BASE_LOG(astring('#', 76));
266 // create a window so that installshield won't barf. this is only needed
267 // on windows when using this as a prerequisite for installshield.
268 window_handle f_window = create_simplistic_window("temp_stubby_class",
269 "stubby window title");
272 // read position for manifest offset out of our array.
273 byte_array temp_packed(2 * sizeof(int), MANIFEST_OFFSET_ARRAY + 8);
274 un_int manifest_offset;
275 if (!structures::obscure_detach(temp_packed, manifest_offset)) {
276 show_message(astring("could not read manifest offset in: ") + _global_argv[0],
281 filename this_exe(_global_argv[0]);
282 if (!this_exe.exists()) {
283 show_message(astring("could not access this exe image: ") + this_exe.raw(),
288 // start reading the manifest...
289 byte_filer our_exe(this_exe, "rb");
290 our_exe.seek(manifest_offset); // go to where the manifest starts.
292 // get number of chunks in manifest as the first bytes.
293 if (our_exe.read(temp_packed, 2 * sizeof(int)) <= 0) {
294 show_message(astring("could not read the manifest length in: ")
295 + this_exe.raw(), ERROR_TITLE);
299 structures::obscure_detach(temp_packed, item_count);
300 //check result of detach!
301 _manifest.insert(0, item_count); // add enough spaces for our item list.
303 // read each item definition out of the manifest now.
304 for (int i = 0; i < (int)item_count; i++) {
305 manifest_chunk &curr = _manifest[i];
306 bool worked = manifest_chunk::read_manifest(our_exe, curr);
310 curr.text_form(tmpork);
311 LOG(a_sprintf("item %d: ", i) + tmpork);
315 show_message(a_sprintf("could not read chunk for item #%d [%s]: ", i,
317 + this_exe.raw(), ERROR_TITLE);
323 LOG("read the following info from manifest:");
325 for (int i = 0; i < _manifest.length(); i++) {
326 manifest_chunk &curr = _manifest[i];
327 temp += a_sprintf("(%d) size %d, %s\n", i, curr._size,
330 critical_events::alert_message(temp, "manifest contents");
333 // now we should be just after the last byte of the manifest, at the
334 // first piece of data. we should read each chunk of data out and store
335 // it where it's supposed to go.
336 for (int festdex = 0; festdex < _manifest.length(); festdex++) {
337 manifest_chunk &curr = _manifest[festdex];
338 int size_left = curr._size;
340 // patch in our environment variables.
341 curr._payload = parser_bits::substitute_env_vars(curr._payload, false);
342 curr._parms = parser_bits::substitute_env_vars(curr._parms, false);
344 BASE_LOG(astring("processing ") + curr._payload
345 + a_sprintf(", size=%d, flags=%d", curr._size, curr._flags));
347 BASE_LOG(astring(" parms: ") + curr._parms);
350 // see if they specified a keyword on the command line.
351 bool keyword_good = true;
352 if (!!keyword && !curr._keywords.member(keyword)) {
353 // their keyword choice didn't match what we were pickled with.
354 keyword_good = false;
355 //BASE_LOG(astring("skipping ") + curr._payload + " for wrong keyword " + keyword);
358 if (curr._flags & SET_VARIABLE) {
360 // this is utterly different from a real target. we just set the
361 // variable and move on.
362 if (provided_target && (curr._payload == TARGET_WORD) ) {
363 BASE_LOG(astring("skipping ") + curr._payload + "=" + curr._parms
364 + ": was provided explicitly as " + target);
365 } else if (_variables.find(curr._payload)) {
366 BASE_LOG(astring("skipping ") + curr._payload + "=" + curr._parms
367 + ": was provided on command line.");
369 BASE_LOG(astring("setting ") + curr._payload + "=" + curr._parms);
370 environment::set(curr._payload, curr._parms);
372 // special code for changing logging directory midstream.
373 if (curr._payload == LOGDIR_WORD) {
374 astring logdir = curr._parms;
375 astring appname = filename(application_configuration::application_name()).rootname();
376 astring logname = logdir + "/" + appname + ".log";
377 standard_log_base *old_log = program_wide_logger::set(new combo_logger(logname));
378 /// log_base *old_log = set_PW_logger_for_combo(logname);
381 if (curr._payload == TARGET_WORD) {
382 // well we've now seen this defined.
383 provided_target = true;
388 } else if (curr._flags & TEST_VARIABLE_DEFINED) {
390 astring var_value = environment::get(curr._payload);
391 if (var_value.empty()) {
392 BASE_LOG(astring("assertion failed: ") + curr._payload + " is not defined!");
393 show_message(a_sprintf("failed test for variable %s: it is "
394 "*not* defined, for item #%d.", curr._payload.s(), festdex),
398 BASE_LOG(astring("assertion succeeded: ") + curr._payload + " defined as " + var_value);
403 if (! (curr._flags & OMIT_PACKING) ) {
404 // this one has a payload, so install it now if appropriate.
405 if (!provided_target) {
407 BASE_LOG(astring("No TARGET has been specified; please provide one on the command line.") + parser_bits::platform_eol_to_chars());
408 return print_instructions();
411 // make sure that the directories needed are present for the outputs.
412 filename target_dir = filename(curr._payload).dirname();
414 if (keyword_good && !target_dir.exists()
415 && !directory::recursive_create(target_dir)) {
416 LOG(a_sprintf("failed to create directory %s for item #%d: ",
417 target_dir.raw().s(), festdex) + curr._payload);
420 // test whether they wanted to allow overwriting.
421 if (curr._flags & NO_OVERWRITE) {
422 filename target_file(curr._payload);
423 if (target_file.exists()) {
424 BASE_LOG(astring("not overwriting existing ") + curr._payload);
425 keyword_good = false;
429 // see if this is supposed to be backed up before installation.
430 if (curr._flags & MAKE_BACKUP_FILE) {
431 filename target_file(curr._payload);
432 if (target_file.exists()) {
433 astring new_file_name = find_unique_backup_name(curr._payload);
434 if (!new_file_name) {
435 BASE_LOG(astring("failed to calculate new filename for ") + curr._payload);
436 keyword_good = false; // cancel the overwrite, couldn't backup.
438 // make a backup of the file by moving the old file name to the
439 // new file name, which should be unique, and then the current
440 // name is all clear to be written as a new file.
441 // BASE_LOG(astring("backing up ") + curr._payload + " --> " + new_file_name);
442 int retval = rename(curr._payload.s(), new_file_name.s());
444 BASE_LOG(astring("failed to rename ") + curr._payload + " as " + new_file_name);
445 keyword_good = false; // cancel the overwrite, couldn't backup.
451 byte_filer *targo = NULL_POINTER;
452 if (keyword_good) targo = new byte_filer(curr._payload, "wb");
453 byte_array uncompressed(256 * KILOBYTE); // fluff it out to begin with.
454 byte_array temp(256 * KILOBYTE);
456 bool first_read = true;
457 // true if there haven't been any reads of the file before now.
458 bool too_tiny_complaint_already = false;
459 // becomes true if we complain about the file's size being larger than
460 // expected. this allows us to only complain once about each file.
462 // read a chunk at a time out of our exe image and store it into the
464 while (first_read || !our_exe.eof()) {
466 un_int real_size = 0, packed_size = 0;
467 // read in the real size from the file.
468 bool worked = manifest_chunk::read_an_obscured_int(our_exe, real_size);
470 show_message(a_sprintf("failed while reading real size "
471 "for item #%d: ", festdex) + curr._payload, ERROR_TITLE);
474 // read in the packed size now.
475 worked = manifest_chunk::read_an_obscured_int(our_exe, packed_size);
477 show_message(a_sprintf("failed while reading packed size "
478 "for item #%d: ", festdex) + curr._payload, ERROR_TITLE);
482 // make sure we don't eat the whole package--did the file end?
483 if ( (real_size == -1) && (packed_size == -1) ) {
484 // we've hit our sentinel; we've already unpacked all of this file.
489 BASE_LOG(a_sprintf("chunk packed_size=%d, real_size=%d", packed_size,
493 // now we know how big our next chunk is, so we can try reading it.
495 int ret = our_exe.read(temp, packed_size);
497 show_message(a_sprintf("failed while reading item #%d: ", festdex)
498 + curr._payload, ERROR_TITLE);
500 } else if (ret != packed_size) {
501 show_message(a_sprintf("bad trouble ahead, item #%d had different "
502 " size on read (expected %d, got %d): ", festdex, packed_size,
503 ret) + curr._payload, ERROR_TITLE);
506 uncompressed.reset(real_size + KILOBYTE); // add some for paranoia.
507 uLongf destlen = uncompressed.length();
508 int uncomp_ret = uncompress(uncompressed.access(), &destlen,
509 temp.observe(), packed_size);
510 if (uncomp_ret != Z_OK) {
511 show_message(a_sprintf("failed while uncompressing item #%d: ",
512 festdex) + curr._payload, ERROR_TITLE);
516 if (int(destlen) != real_size) {
517 LOG(a_sprintf("got a different unpacked size for item #%d: ",
518 festdex) + curr._payload);
521 // update the remaining size for this data chunk.
522 size_left -= real_size;
524 if (!too_tiny_complaint_already) {
525 LOG(a_sprintf("item #%d was larger than expected (non-fatal): ",
526 festdex) + curr._payload);
527 too_tiny_complaint_already = true;
530 // toss the extra bytes out.
531 uncompressed.zap(real_size, uncompressed.length() - 1);
534 // stuff the data we read into the target file.
535 ret = targo->write(uncompressed);
536 if (ret != uncompressed.length()) {
537 show_message(a_sprintf("failed while writing item #%d: ", festdex)
538 + curr._payload, ERROR_TITLE);
544 if (targo) targo->close();
546 // the file's written, but now we slap it's old time on it too.
548 if (!t.unpack(curr.c_filetime)) {
549 show_message(astring("failed to interpret timestamp for ")
550 + curr._payload, ERROR_TITLE);
553 // put the timestamp on the file.
554 t.set_time(curr._payload);
555 // utimbuf held_time;
556 // held_time.actime = t.raw();
557 // held_time.modtime = t.raw();
558 // // put the timestamp on the file.
559 // utime(curr._payload.s(), &held_time);
562 // now that we're pretty sure the file exists, we can run it if needed.
563 if ( (curr._flags & TARGET_EXECUTE) && keyword_good) {
564 // change the mode on the target file so we can execute it.
565 chmod(curr._payload.s(), 0766);
566 astring prev_dir = application_configuration::current_directory();
568 BASE_LOG(astring("launching ") + curr._payload);
570 BASE_LOG(astring(" with parameters: ") + curr._parms);
571 BASE_LOG(astring('-', 76));
574 basis::un_int retval = launch_process::run(curr._payload, curr._parms,
575 launch_process::AWAIT_APP_EXIT | launch_process::HIDE_APP_WINDOW, kid);
577 if (! (curr._flags & IGNORE_ERRORS) ) {
578 if (curr._flags & QUIET_FAILURE) {
579 // no message box for this, but still log it.
580 LOG(astring("failed to launch process, targ=")
581 + curr._payload + " with parms " + curr._parms
582 + a_sprintf(" error=%d", retval));
584 show_message(astring("failed to launch process, targ=")
585 + curr._payload + " with parms " + curr._parms
586 + a_sprintf(" error=%d", retval), ERROR_TITLE);
588 return retval; // pass along same exit value we were told.
590 LOG(astring("ignoring failure to launch process, targ=")
591 + curr._payload + " with parms " + curr._parms
592 + a_sprintf(" error=%d", retval));
596 chdir(prev_dir.s()); // reset directory pointer, just in case.
598 BASE_LOG(astring('-', 76));
604 whack_simplistic_window(f_window);
610 ////////////////////////////////////////////////////////////////////////////
612 HOOPLE_MAIN(unpacker_stub, )
614 #ifdef __BUILD_STATIC_APPLICATION__
615 // static dependencies found by buildor_gen_deps.sh:
616 #include <application/application_shell.cpp>
617 #include <application/command_line.cpp>
618 #include <basis/astring.cpp>
619 #include <basis/common_outcomes.cpp>
620 #include <basis/environment.cpp>
621 #include <basis/mutex.cpp>
622 #include <basis/utf_conversion.cpp>
623 #include <configuration/application_configuration.cpp>
624 #include <configuration/configurator.cpp>
625 #include <configuration/ini_configurator.cpp>
626 #include <configuration/ini_parser.cpp>
627 #include <configuration/table_configurator.cpp>
628 #include <configuration/variable_tokenizer.cpp>
629 #include <filesystem/byte_filer.cpp>
630 #include <filesystem/directory.cpp>
631 #include <filesystem/file_info.cpp>
632 #include <filesystem/filename.cpp>
633 #include <filesystem/filename_list.cpp>
634 #include <filesystem/file_time.cpp>
635 #include <filesystem/heavy_file_ops.cpp>
636 #include <filesystem/huge_file.cpp>
637 #include <loggers/combo_logger.cpp>
638 #include <loggers/console_logger.cpp>
639 #include <loggers/critical_events.cpp>
640 #include <loggers/file_logger.cpp>
641 #include <loggers/program_wide_logger.cpp>
642 #include <processes/launch_process.cpp>
643 #include <structures/bit_vector.cpp>
644 #include <structures/checksums.cpp>
645 #include <structures/object_packers.cpp>
646 #include <structures/static_memory_gremlin.cpp>
647 #include <structures/string_hasher.cpp>
648 #include <structures/string_table.cpp>
649 #include <structures/version_record.cpp>
650 #include <textual/byte_formatter.cpp>
651 #include <textual/parser_bits.cpp>
652 #include <textual/string_manipulation.cpp>
653 #include <timely/earth_time.cpp>
654 #include <timely/time_control.cpp>
655 #include <timely/time_stamp.cpp>
656 #endif // __BUILD_STATIC_APPLICATION__