X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/mup/blobdiff_plain/cdb3c0882392596f814cf939cbfbd38adc6f2bfe..ddf6330b56bcfb657e0186b24b9b1422c51d3424:/mup/mupmate/Run.C diff --git a/mup/mupmate/Run.C b/mup/mupmate/Run.C new file mode 100644 index 0000000..de95228 --- /dev/null +++ b/mup/mupmate/Run.C @@ -0,0 +1,1371 @@ +/* Copyright (c) 2006 by Arkkra Enterprises */ +/* All rights reserved */ + +// This file includes methods related to the "Run" menu on the main toolbar. + +#include "Run.H" +#include "Preferences.H" +#include "globals.H" +#include "Config.H" +#include "defines.h" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef OS_LIKE_UNIX +#include +#include +#endif + +// Message for when Mup fails, but we can't figure out why. +const char * Unknown_Mup_failure = "Mup failed. Reason unknown."; + +// Describe use of numbers passed to -x option +#define EXTRACT_NUM_DESCRIPTION "0 is used for pickup measure.\n" \ + "Negative numbers are used to specify\n" \ + "number of measures from the end.\n" + +// Tooltip is the same for all macro entries +const char * macro_definition_tip = + "Your Mup input can use macros to vary characteristics\n" + "of the output for different runs.\n" + "Enter the name of a macro you want to define,\n" + "optionally followed by an equals sign and macro definition.\n" + "Macro names must consist of upper case letters,\n" + "numbers, and underscores, starting with an upper case letter."; + +//------ Class for user to set parameters to pass to Mup + +Run_parameters_dialog::Run_parameters_dialog(void) + : Fl_Double_Window(0, 0, 700, 350, "Mup Options") +{ + enable_combine_p = new Fl_Check_Button(20, 20, 165, 30, + "Enable Auto Multirest"); + enable_combine_p->tooltip("Set whether to automatically combine\n" + "consecutive measures of rests into a multirest."); + enable_combine_p->callback(combine_cb, this); + enable_combine_p->when(FL_WHEN_CHANGED); + + rest_combine_p = new Positive_Int_Input(370, 20, 40, 30, + "Min Measures to Combine"); + rest_combine_p->tooltip("Minimum number of consecutive measures\n" + "of rest that will be combined into a multirest."); + // Only becomes active if auto-multirest combine becomes active + rest_combine_p->deactivate(); + + // -p firstpage + first_page_p = new Positive_Int_Input(300, 60, 50, 30, "First Page's Page Number"); + first_page_p->tooltip("Set the page number to use\n" + "for the first page of music output."); + + // -o pagelist + pages_p = new Fl_Group(30, 100, 380, 70, "Pages to Display"); + pages_p->box(FL_ENGRAVED_BOX); + pages_p->align(FL_ALIGN_TOP_LEFT | FL_ALIGN_INSIDE); + + all_p = new Fl_Check_Button(170, 105, 55, 20, "All"); + all_p->type(FL_RADIO_BUTTON); + all_p->tooltip("You can restrict to displaying only a subset\n" + "of the pages of music, but this shows all pages."); + all_p->callback(selected_pages_cb, this); + all_p->when(FL_WHEN_CHANGED); + + odd_p = new Fl_Check_Button(245, 105, 60, 20, "Odd"); + odd_p->type(FL_RADIO_BUTTON); + odd_p->tooltip("This restricts to displaying only\n" + "odd numbered pages. This is generally\n" + "most useful for printing to a\n" + "single-sided printer."); + odd_p->callback(selected_pages_cb, this); + odd_p->when(FL_WHEN_CHANGED); + + even_p = new Fl_Check_Button(320, 105, 70, 20, "Even"); + even_p->type(FL_RADIO_BUTTON); + even_p->tooltip("This restricts to displaying only\n" + "even numbered pages. This is generally\n" + "most useful for printing to a\n" + "single-sided printer."); + even_p->callback(selected_pages_cb, this); + even_p->when(FL_WHEN_CHANGED); + + selected_p = new Fl_Check_Button(50, 135, 85, 20, "Selected:"); + selected_p->tooltip("This restricts to displaying only\n" + "the pages you list in the blank to the right."); + selected_p->type(FL_RADIO_BUTTON); + selected_p->callback(selected_pages_cb, this); + selected_p->when(FL_WHEN_CHANGED); + + page_list_p = new Fl_Input(150, 130, 230, 30, ""); + page_list_p->tooltip("List the specific pages you want displayed.\n" + "You can list individual pages separated by commas,\n" + "and/or ranges of pages separated by dashes.\n" + "Pages may be listed more than once and in any order."); + page_list_p->deactivate(); + saved_page_list = 0; + pages_p->end(); + + // -s stafflist + staff_list_p = new Fl_Input(150, 180, 260, 30, "Staffs to Display/Play"); + staff_list_p->tooltip("If you wish to display or play only a subset\n" + "of the staffs, or voices on a staff, list them here.\n" + "Staffs are specified by comma-separated numbers\n" + "or ranges, like 1,4 or 1-3,5-6. Staff numbers can be\n" + "followed by v1, v2, or v3 to limit to a single voice.\n"); + saved_staff_list = 0; + + // -x extract list + extract_begin_p = new Int_Input(150, 220, 95, 30, "Extract Measures"); + extract_begin_p->tooltip("If you wish to display or play only selected\n" + "measures, enter the starting measure here.\n" + EXTRACT_NUM_DESCRIPTION); + extract_begin_p->callback(extract_cb, this); + extract_begin_p->when(FL_WHEN_CHANGED); + extract_end_p = new Int_Input(315, 220, 95, 30, "through"); + extract_end_p->tooltip("If you wish to display or play only selected\n" + "measures, enter the ending measure here.\n" + EXTRACT_NUM_DESCRIPTION); + extract_end_p->deactivate(); + + // Macros + macros_group_p = new Fl_Group(430, 20, 250, 40 + MAX_MACROS * 40, "Macro Definitions"); + macros_group_p->box(FL_ENGRAVED_BOX); + macros_group_p->align(FL_ALIGN_TOP | FL_ALIGN_INSIDE); + int m; + for (m = 0; m < MAX_MACROS; m++) { + + macro_definitions_p[m] = new Fl_Input(450, 50 + m * 40, 210, 30, ""); + macro_definitions_p[m]->tooltip(macro_definition_tip); + + saved_macro_definitions[m] = 0; + } + macros_group_p->end(); + + save_p = new Fl_Return_Button(100, h() - 50, 80, 30, "Save"); + save_p->callback(Save_cb, this); + save_p->when(FL_WHEN_RELEASE); + + clear_form_p = new Fl_Button((w() - 150) / 2, h() - 50, 150, 30, + "Clear Options"); + clear_form_p->callback(clear_form_cb, this); + clear_form_p->when(FL_WHEN_RELEASE); + + cancel_p = new Fl_Button(w() - 180, h() - 50, 80, 30, "Cancel"); + cancel_p->shortcut(FL_Escape); + cancel_p->callback(Cancel_cb, this); + cancel_p->when(FL_WHEN_RELEASE); + + // Set everything to default values + clear_form(); + + // Arrange to clean up all the new-ed widgets in destructor. + end(); + + // Arrange for window manager closes to do Cancel. + callback(Cancel_cb, this); + when(FL_WHEN_NEVER); +} + + +Run_parameters_dialog::~Run_parameters_dialog(void) +{ + int m; + for (m = 0; m < MAX_MACROS; m++) { + if (saved_macro_definitions[m] != 0) { + delete saved_macro_definitions[m]; + } + } +} + + +//---- Callback for when user changes enablement of rest combining, +// to gray or ungray the field for how many measures to combine. + +CALL_BACK(Run_parameters_dialog, combine) +{ + if (enable_combine_p->value()) { + rest_combine_p->activate(); + } + else { + rest_combine_p->value(""); + rest_combine_p->deactivate(); + } +} + + +//---- Callback for when user changes setting of start of measure extraction, +// to gray or ungray the field for ending measure of extraction. + +CALL_BACK(Run_parameters_dialog, extract) +{ + if (extract_begin_p->size() > 0) { + extract_end_p->activate(); + } + else { + extract_end_p->value(""); + extract_end_p->deactivate(); + } +} + +//---- Callback for when user changes setting of page selection, +// to gray or ungray the field for listing the specific pages to display + +CALL_BACK(Run_parameters_dialog, selected_pages) +{ + page_list_p->value(""); + if (selected_p->value() == 1) { + page_list_p->activate(); + } + else { + page_list_p->deactivate(); + } +} + + +//----Callback for button for clearing the form + +CALL_BACK(Run_parameters_dialog, clear_form) +{ + // Set everything to default values + saved_enable_combine = false; + enable_combine_p->value(saved_enable_combine); + + (void) sprintf(saved_combine_measures, ""); + rest_combine_p->value(saved_combine_measures); + + (void) sprintf(saved_first_page, "%d", MINFIRSTPAGE); + first_page_p->value(saved_first_page); + + if (saved_page_list != 0) { + delete saved_page_list; + } + saved_page_list = new char[1]; + saved_page_list[0] = '\0'; + page_list_p->value(saved_page_list); + all_p->value(1); + saved_pages = ALL_PAGES; + + if (saved_staff_list != 0) { + delete saved_staff_list; + } + saved_staff_list = new char[1]; + saved_staff_list[0] = '\0'; + staff_list_p->value(saved_staff_list); + + saved_extract_begin[0] = '\0'; + extract_begin_p->value(saved_extract_begin); + saved_extract_end[0] = '\0'; + extract_end_p->value(saved_extract_end); + + int m; + for (m = 0; m < MAX_MACROS; m++) { + macro_definitions_p[m]->value(""); + if (saved_macro_definitions[m] != 0) { + delete saved_macro_definitions[m]; + saved_macro_definitions[m] = 0; + } + } +} + + +//---- callback for when user clicks "Save" on Set Options form + +CALL_BACK(Run_parameters_dialog, Save) +{ + // -c rest combine option + bool error = false; + saved_enable_combine = enable_combine_p->value(); + int num_meas = (int) atoi(rest_combine_p->value()); + if (saved_enable_combine && (num_meas < MINRESTCOMBINE || num_meas > MAXRESTCOMBINE)) { + fl_alert("\"Min Measures to Combine\" must be between\n" + "%d and %d.", MINRESTCOMBINE, MAXRESTCOMBINE); + error = true; + } + else { + (void) strcpy(saved_combine_measures, rest_combine_p->value()); + } + + // -p first page option + int page_num = (int) atoi(first_page_p->value()); + if (page_num < MINFIRSTPAGE || page_num > MAXFIRSTPAGE) { + fl_alert("\"First Page\" number must be between\n" + "%d and %d.", MINFIRSTPAGE, MAXFIRSTPAGE); + error = true; + } + else { + (void) strcpy(saved_first_page, first_page_p->value()); + } + + // We don't really fully parse and error check the -o argument, + // but make sure it at least is made up of only valid characters. + if (page_list_p->size() > 0) { + if (strspn(page_list_p->value(), "0123456789,- \t") + != page_list_p->size()) { + fl_alert("\"Pages to Display\" value is not valid."); + error = true; + } + else { + // Free existing, if any, and save new value. + if (saved_page_list != 0) { + delete saved_page_list; + } + saved_page_list = new char[page_list_p->size() + 1]; + strcpy(saved_page_list, page_list_p->value()); + } + } + else if (saved_page_list[0] != '\0') { + // Had a list before but not anymore. + // Null out the existing list. + delete saved_page_list; + saved_page_list = new char[1]; + saved_page_list[0] = '\0'; + } + if (all_p->value() == 1) { + saved_pages = ALL_PAGES; + } + if (odd_p->value() == 1) { + saved_pages = ODD_PAGES; + } + if (even_p->value() == 1) { + saved_pages = EVEN_PAGES; + } + if (selected_p->value() == 1) { + saved_pages = SELECTED_PAGES; + } + if (saved_pages == SELECTED_PAGES && page_list_p->size() == 0) { + fl_alert("You did not list which selected pages you want."); + error = true; + } + + // Similar for staff list (-s option) + if (staff_list_p->size() > 0) { + if (strspn(staff_list_p->value(), "0123456789,-v \t") + != staff_list_p->size()) { + fl_alert("\"Staffs to Display/Play\" is not valid"); + error = true; + } + else { + if (saved_staff_list != 0) { + delete saved_staff_list; + } + saved_staff_list = new char[staff_list_p->size() + 1]; + strcpy(saved_staff_list, staff_list_p->value()); + } + } + else if (saved_staff_list[0] != '\0') { + delete saved_staff_list; + saved_staff_list = new char[1]; + saved_staff_list[0] = '\0'; + } + + // We can't really error check -x option values. + // Widget will have already constrained to integer, + // and we have no way to know what range is valid, + // since we don't know how many measures the song has. + // However, to keep things simple, we have fixed-size array of 8 bytes + // for saving value, so can only allow up to 6 digits plus sign. + if (abs(atoi(extract_begin_p->value())) >= 1000000) { + fl_alert("\"Extract Measures\" start value is out of range."); + error = true; + } + else { + (void) strcpy(saved_extract_begin, extract_begin_p->value()); + } + if (abs(atoi(extract_end_p->value())) >= 1000000) { + fl_alert("\"Extract Measures\" end value is out of range."); + error = true; + } + else { + (void) strcpy(saved_extract_end, extract_end_p->value()); + } + + // Macros + int m; + int macsize; + bool changed; + for (m = 0; m < MAX_MACROS; m++) { + changed = false; + if ((macsize = macro_definitions_p[m]->size()) > 0) { + if (macro_error(macro_definitions_p[m]->value())) { + fl_alert("Macro %d has invalid format.", m + 1); + error = true; + } + else if (saved_macro_definitions[m] == 0) { + // no macro before, but is one now + changed = true; + } + else if (strcmp(macro_definitions_p[m]->value(), + saved_macro_definitions[m]) != 0) { + // A different macro value than before + delete saved_macro_definitions[m]; + changed = true; + } + if (changed) { + saved_macro_definitions[m] = new char[macsize + 1]; + (void) strcpy(saved_macro_definitions[m], + macro_definitions_p[m]->value()); + } + } + else if (macsize == 0 && saved_macro_definitions[m] != 0) { + // Used to be a macro value, but not anymore + delete saved_macro_definitions[m]; + saved_macro_definitions[m] = 0; + } + } + + + // If there were user errors, we leave the window up for user to + // correct the errors. User can, of course, cancel without correcting, + // in which case if they try to run, Mup will complain about the + // bad arguments. + if ( ! error ) { + hide(); + } +} + + +//---- Callback for when user clicks "Cancel" on Set Options form + +CALL_BACK(Run_parameters_dialog, Cancel) +{ + // Put back all the previous values + enable_combine_p->value(saved_enable_combine); + rest_combine_p->value(saved_combine_measures); + first_page_p->value(saved_first_page); + + // It seems setting a radio button via value(1) doesn't reset the + // others, so clear all, then set the one we want + all_p->value(0); + odd_p->value(0); + even_p->value(0); + selected_p->value(0); + switch (saved_pages) { + default: // default should be impossible + case ALL_PAGES: + all_p->value(1); + break; + case ODD_PAGES: + odd_p->value(1); + break; + case EVEN_PAGES: + even_p->value(1); + break; + case SELECTED_PAGES: + selected_p->value(1); + break; + } + page_list_p->value(saved_page_list); + staff_list_p->value(saved_staff_list); + extract_begin_p->value(saved_extract_begin); + extract_end_p->value(saved_extract_end); + + int m; + for (m = 0; m < MAX_MACROS; m++) { + macro_definitions_p[m]->value( saved_macro_definitions[m] == 0 + ? "" : saved_macro_definitions[m]); + } + hide(); +} + +// Return true if check of macro finds an error. + +bool +Run_parameters_dialog::macro_error(const char * macro) +{ + // First character must be an upper case letter. + if ( ! isupper(macro[0])) { + return(true); + } + // Everything up to = or end of string, whichever comes first, + // must be upper case letter, digit, or underscore. + const char * m_p; + for (m_p = macro; *m_p != '\0' && *m_p != '='; m_p++) { + if ( ! isupper(*m_p) && ! isdigit(*m_p) && *m_p != '_') { + return(true); + } + } + return(false); +} + + +//--------class for Run menu ----------------------------------------------- + +Run::Run() +{ + parameters_p = 0; + report_p = 0; +#ifdef OS_LIKE_WIN32 + display_child.hProcess = 0; + display_child.dwProcessId = 0; + MIDI_child.hProcess = 0; + MIDI_child.dwProcessId = 0; +#else + display_child = 0; + MIDI_child = 0; +#endif +} + +Run::~Run() +{ + if (parameters_p != 0) { + delete parameters_p; + parameters_p = 0; + } + if (report_p != 0) { + delete report_p; + report_p = 0; + } + // Kill off any child processes + clean_up(); +} + + + +//------------callback for Display menu item + +CALL_BACK(Run, Display) +{ + Run_Mup(false, true); +} + + +//-----Callback for menu item to play MIDI file + +CALL_BACK(Run, Play) +{ + Run_Mup(true, true); +} + + +//---------callback for menu item for writing PostScript output + +CALL_BACK(Run, WritePostScript) +{ + Run_Mup(false, false); +} + +//---------callback for menu item for writing MIDI output + +CALL_BACK(Run, WriteMIDI) +{ + Run_Mup(true, false); +} + +//---- callback for menu item to collect Mup command line parameters from user + +CALL_BACK(Run, Options) +{ + if (parameters_p == 0) { + parameters_p = new Run_parameters_dialog(); + } + parameters_p->show(); +} + + +//--------- This lets Run class know the file name and editor buffer, +// which it needs access to, so it can run Mup on its contents. + +void +Run::set_file(File * file_info_p) +{ + file_p = file_info_p; +} + + +// Run Mup commands with user's parameters on the current file + +void +Run::Run_Mup(bool midi, bool show_or_play) +{ + + const char * mup_input = file_p->effective_filename(); + // Make sure file has been written. + // The fiilename == 0 will be hit in the case where + // user did New From Template but made no changes. + if (file_p->unsaved_changes || file_p->filename == 0) { + int auto_save; + (void) Preferences_p->get(Auto_save_preference, auto_save, + Default_auto_save); + if (auto_save) { + file_p->Save(false); + if (file_p->unsaved_changes || file_p->filename == 0) { + // User probably canceled a Save that got + // turned into a SaveAs, + // or maybe Save failed. If not properly + // saved, we shouldn't try to process it. + return; + } + } + else { + bool hide_the_No; + const char * extra_text; + // If there is a previous version of the file, + // allow user to select "No," which means use that + // previous version rather than writing out current. + if (access(mup_input, F_OK) == 0) { + hide_the_No = false; + extra_text = "(If you select \"No,\" " + "the most recently saved version " + "of the Mup input file will be used.)"; + } + else { + // No previous version + hide_the_No = true; + extra_text = ""; + } + switch (file_p->save_changes_check(extra_text, hide_the_No)) { + default: // default case should be impossible + case Save_confirm_dialog::Cancel: + return; + case Save_confirm_dialog::No: + break; + case Save_confirm_dialog::Yes: + file_p->Save(false); + if (file_p->unsaved_changes + || file_p->filename == 0) { + // User probably canceled the Save, + // or maybe it failed. If not properly + // saved, we shouldn't try to process it. + return; + } + break; + } + } + // If was "Untitled" before, it is now saved under a good + // name, so get what that is. + mup_input = file_p->effective_filename(); + } + + // Get length of file name without the extension + int base_length = strlen(mup_input) + - strlen(fl_filename_ext(mup_input)); + + // Create output file name + char mup_output[base_length + (midi ? 5 : 4)]; + strncpy(mup_output, mup_input, base_length); + strcpy(mup_output + base_length, (midi ? ".mid" : ".ps")); + + // Create error file name + char mup_error[base_length + 5]; + strncpy(mup_error, mup_input, base_length); + strcpy(mup_error + base_length, ".err"); + + // Get Mup command to use. + char * mup_command; + (void) Preferences_p->get(Mup_program_location, mup_command, + Default_Mup_program_location); + char full_location[FL_PATH_MAX]; + if ( ! find_executable(mup_command, full_location)) { + fl_alert("Mup command not found.\n" + "Check Config > File Locations setting."); + return; + } + + if (parameters_p == 0) { + // User hasn't set any parameters, so we will use all + // defaults. Creating the instance lets us deference it + // below, so we don't have to care whether user ever + // went to the Set Options page or not. + parameters_p = new Run_parameters_dialog(); + } + + // Build up list of arguments. + // array slots needed for args: + // 1 for Mup command itself + // 2 for -e and arg + // 2 for -f or -m and arg + // 2 for -c and arg + // 2 for -p and arg + // 2 for -o and arg + // 2 for -s and arg + // 2 for -x and arg + // 1 for Mup input file name + // 2 for each -D and its macro definition arg + // 1 for null terminator + const char * command[17 + 2 * MAX_MACROS]; + command[0] = full_location; + command[1] = "-e"; + command[2] = mup_error; + command[3] = (midi ? "-m" : "-f"); + command[4] = mup_output; + int arg_offset = 5; + + // rest combine + if (parameters_p->enable_combine_p->value() && + parameters_p->rest_combine_p->size() > 0) { + command[arg_offset++] = "-c"; + command[arg_offset++] = parameters_p->rest_combine_p->value(); + } + + // first page + if (parameters_p->first_page_p->size() > 0) { + command[arg_offset++] = "-p"; + command[arg_offset++] = parameters_p->first_page_p->value(); + } + + // page list + switch (parameters_p->saved_pages) { + default: // default should be impossible + case Run_parameters_dialog::ALL_PAGES: + break; + case Run_parameters_dialog::ODD_PAGES: + command[arg_offset++] = "-o"; + command[arg_offset++] = "odd"; + break; + case Run_parameters_dialog::EVEN_PAGES: + command[arg_offset++] = "-o"; + command[arg_offset++] = "even"; + case Run_parameters_dialog::SELECTED_PAGES: + if (parameters_p->page_list_p->size() > 0) { + command[arg_offset++] = "-o"; + command[arg_offset++] = parameters_p->page_list_p->value(); + } + // Else they said selected, but then didn't list + // any particular pages. We treat like ALL_PAGES, + // since no pages is useless. + // We would have already given error message, + // but they must have ignored it. + break; + } + + // staff list + if (parameters_p->staff_list_p->size() > 0) { + command[arg_offset++] = "-s"; + command[arg_offset++] = parameters_p->staff_list_p->value(); + } + + // extract list + char xoption[parameters_p->extract_begin_p->size() + + parameters_p->extract_end_p->size() + 2]; + if (parameters_p->extract_begin_p->size() > 0) { + command[arg_offset++] = "-x"; + (void) strcpy(xoption, parameters_p->extract_begin_p->value()); + if (parameters_p->extract_end_p->size() > 0) { + (void) strcat(xoption, ","); + (void) strcat(xoption, parameters_p->extract_end_p->value()); + } + command[arg_offset++] = xoption; + } + + // -D options + int m; + for (m = 0; m < MAX_MACROS; m++) { + if (parameters_p->saved_macro_definitions[m] != 0) { + command[arg_offset++] = "-D"; + command[arg_offset++] = parameters_p->saved_macro_definitions[m]; + } + } + + // Mup input file name and null terminator + command[arg_offset++] = mup_input; + command[arg_offset++] = 0; + + static bool set_mupquiet = false; + if ( ! set_mupquiet ) { + putenv("MUPQUIET=1"); + set_mupquiet = true; + } + + // Look up the right (dis)player program to use. + // On Windows we need this even if we are only writing the file, + // not actually (dis)playing. To keep the code simpler, + // we just always look it up. + char * player_command; + if (midi) { + (void) Preferences_p->get(MIDI_player_location, + player_command, + Default_MIDI_player_location); + } + else { + (void) Preferences_p->get(Viewer_location, + player_command, + Default_viewer_location); + } +#ifdef OS_LIKE_WIN32 + // Media player locks the file it plays, so if we had + // run it before, we have to make it release the lock + // before we run Mup. Also, if it was playing a different file + // before, it doesn't seem to want to play ours. + // So if the MIDI player of choice is the media player, + // we first try to make it close somewhat gracefully, + // and wait a second for that to complete. + // In any case we try to kill off any child MIDI players we + // know of. If the graceful close already worked, + // it should find the child already dead. + // If they are using some other MIDI player, + // we don't know what to do, so we'll just kill any child + // MIDI player we know spawned previously, if any. + // Would be nice to be able to do something less drastic than + // kill the process, but we're not sure what else would be effective. + if (midi) { + if (is_mplayer(player_command)) { + HWND window_handle; + if ((window_handle = FindWindow(0, "Windows Media Player")) != 0) { + SendMessage(window_handle, WM_CLOSE, 0, 0); + } + _sleep(1000); + } + if (has_MIDI_child()) { + kill_process(&MIDI_child, "MIDI player"); + } + } + // Somewhat similarly, we kill off any prior displayer, unless + // the displayer is GSview, which we know we can pass -e argument + // to make it reuse existing instance, if any. + else if ( ! is_gsview(player_command) && has_display_child()) { + kill_process(&display_child, "display"); + } +#endif + + // Run the command. + int ret = execute_command(command, 0, true); + + // Report the errors, if any. + struct stat info; + if (stat(mup_error, &info) == 0) { + if (info.st_size > 0) { + if (report_p == 0) { + report_p = new Error_report(); + } + report_p->loadfile(mup_error); + report_p->show(); + } + unlink(mup_error); + } + else if (ret != 0) { + // Exited with error, but left no error file. + // Must have died badly (core dumped, execvp failed, etc) +#if defined(WIFEXITED) && defined(WIFSIGNALED) + if (WIFEXITED(ret)) { + // Did exit(), so most likely exec failed + // due to bad path to Mup program, + // although we should have caught that above. + fl_alert("Mup exited with return code %d but no error output.\n" + "Check Config > File Locations setting.", + WEXITSTATUS(ret)); + } + else if (WIFSIGNALED(ret)) { + // Probably core dump :-( + fl_alert("Mup exited due to signal %d.", WTERMSIG(ret)); + } else { + fl_alert(Unknown_Mup_failure); + } +#else // WIF... macros not defined + if (ret == -1) { +#ifdef OS_LIKE_WIN32 + // Look up the error reason to include in message. + DWORD format_retval; + LPVOID error_string = 0; + if (FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_ARGUMENT_ARRAY, + 0, GetLastError(), + LANG_NEUTRAL, (LPTSTR)&error_string, + 0, 0)) { + fl_alert("Failed to execute Mup program:\n%s" + "Check settings in Config > File Locations.", + (char *) error_string); + LocalFree((HLOCAL)error_string); + } + else { + fl_alert(Unknown_Mup_failure); + } + } +#else // not Windows + fl_alert(Unknown_Mup_failure); + } +#endif + else { + fl_alert(Unknown_Mup_failure); + } +#endif // WIF... macros + } + + if (ret != 0) { + // Something went wrong. We should not go on to + // display/play even if user had asked for that. + return; + } + + // We wrote the output file successfully. + // Hide any previous error window and + // check if we need to (dis)play the results. + if (report_p != 0) { + report_p->hide(); + } + if ( ! show_or_play ) { + // User just asked to generate a file, not (dis)play it. + fl_message("%s output file\n\n %s\n\n" + "has been successfully created.", + (midi ? "MIDI" : "PostScript"), mup_output); + return; + } + + + if ( ! find_executable(player_command, full_location)) { + fl_alert("Unable to run %s command.\n" + "Check Config > File Locations setting.", + player_command); + return; + } + +#ifdef OS_LIKE_UNIX + // For MIDI, we try to kill the previous player + // if any, since we'll probably need the same + // sound card. Note that for Windows, we did this + // even before running Mup, since the player may lock + // the file so Mup would be unable to write it. + if (midi) { + kill_process(&MIDI_child, "MIDI player"); + } + + // If using gv as displayer and one already running on + // this file, it would be nice to get send it SIGHUP + // to make it re-read the file + // rather than starting a new one. + // But for other displayers, we don't know what signal + // might work, if any. And even with gv, if we send + // the signal, we have no way of knowing whether it + // actually worked. And there are lots of cases to + // consider. Suppose the displayer was gv, + // and they brought up an instance, + // then they changed to some other displayer, + // and then back to gv. Should we have killed off + // the first gv when bringing up the other viewer, + // of should we keep it up and reuse it after they + // change back? What if they kill the current + // instance just after we decided to re-use it? + // So just keep things simple for now. Always + // kill off any existing viewer we know of and + // start up a new one. + else { // displaying + kill_process(&display_child, "display"); + } +#endif + + // Fill in the argv array for displayer/player + command[0] = full_location; +#ifdef OS_LIKE_WIN32 + // Media player appears to ignore its argument + // if it isn't a full path name, including drive. + // So ensure we have everything. + char fullpath[FL_PATH_MAX]; + if (mup_output[1] != ':' ) { + // We don't have complete path. + // Get current directory including drive. + if (getcwd(fullpath, sizeof(fullpath)) == 0) { + fl_alert("Unable to determine current folder."); + return; + } + if (mup_output[0] != dir_separator()) { + // Relative path + (void) sprintf(fullpath + strlen(fullpath), + "%c%s", dir_separator(), + mup_output); + } + else { + // Was full path except for drive. + (void) strcpy(fullpath + 2, mup_output); + } + } + else { + // Can use existing path as is. + (void) strcpy(fullpath, mup_output); + } + + if ( ! midi && is_gsview(player_command)) { + // For gsview, use -e to make it use + // existing instance if any. + command[1] = "-e"; + command[2] = fullpath; + command[3] = '\0'; + } + else { + command[1] = fullpath; + command[2] = '\0'; + + } +#else + command[1] = mup_output; + command[2] = '\0'; +#endif + + if (execute_command(command, + (midi ? &MIDI_child : &display_child)) != 0) { + fl_alert("Unable to run %s command.\n" + "Check settings under Config > File Locations.", + command[0]); + } +} + + +// Execute given command with the given argv. +// If proc_info_p is zero, wait for the process to complete, +// otherwise save information about the spawned process in what it points to, +// so that the caller can keep track of it while it runs independently. +// The hide_window parameter is only used for Windows and causes the +// spawned process to be created with flags to not create a window, +// This lets us use a console mode version of Mup, so traditional users +// can continue to run Mup in a console without mupmate, but we can +// run the same .exe without the annoyance of a console popping up. +// Returns 0 on success, -1 on failure to create process, +// or what the process returned if it had non-zero exit code, +// and was waited for. + +int +Run::execute_command(const char ** argv, Proc_Info *proc_info_p, bool hide_window) +{ + int ret = -1; + +#ifdef OS_LIKE_UNIX + pid_t child; + switch (child = fork()) { + case 0: + execvp(argv[0], (char * const *)argv); + // If here, the exec failed. Child must die. + exit(1); + case -1: + // failed + if (proc_info_p != 0) { + *proc_info_p = 0; + } + break; + default: + if (proc_info_p == 0) { + // wait for child to complete + if (waitpid(child, &ret, 0) < 0) { + ret = -1; + } + } + else { + *proc_info_p = child; + ret = 0; + } + break; + } +#else + +#ifdef OS_LIKE_WIN32 + + // Convert the argv array into a string with each argument quoted. + // First calculate how much space is needed. + int a; + int length = 0; + bool has_quote = false; // to optimize normal case + for (a = 0; argv[a] != 0; a++) { + length += strlen(argv[a]); + // A quoted argv[0] won't work; just quote the other args. + if (a > 0) { + // Add space before the arg and quotes on each end. + length += 3; + const char * s; + // If embedded quote, add space for escaping it + for (s = argv[a]; *s != '\0'; s++) { + if (*s == '"') { + length++; + has_quote = true; + } + } + } + } + + // Get space and fill in all the arguments, properly quoted. + char command[length + 1]; + (void) strcpy(command, argv[0]); + char * dest = command + strlen(command); + for (a = 1; argv[a] != 0; a++) { + *dest++ = ' '; + *dest++ = '"'; + if (has_quote) { + int i; + for (i = 0; argv[a][i] != '\0'; i++) { + if (argv[a][i] == '"') { + *dest++ = '\\'; + } + *dest++ = argv[a][i]; + } + } + else { + (void) strcpy(dest, argv[a]); + dest += strlen(dest); + } + *dest++ = '"'; + } + *dest = '\0'; + + // Fill in information for starting up the process + PROCESS_INFORMATION process_info; + STARTUPINFO startup_info; + memset( &startup_info, 0, sizeof(startup_info)); + startup_info.cb = sizeof(startup_info); + DWORD create_flags; // flags to control creation aspects + if (hide_window) { + startup_info.dwFlags = STARTF_USESHOWWINDOW; + startup_info.wShowWindow = SW_HIDE; + create_flags = CREATE_NO_WINDOW; + } + else { + create_flags = 0; + } + + // Run the process + BOOL proc = CreateProcess(NULL, command, NULL, NULL, + TRUE, create_flags, NULL, NULL, + &startup_info, &process_info); + + if (proc) { + // It was successfully created. + if (proc_info_p == 0) { + // wait for child to complete + DWORD result = WaitForSingleObject( + process_info.hProcess, INFINITE); + switch (result) { + case WAIT_FAILED: + case WAIT_ABANDONED: + case WAIT_TIMEOUT: + ret = -1; + break; + default: + GetExitCodeProcess(process_info.hProcess, &result); + ret = (int) result; + } + } + else { + *proc_info_p = process_info; + ret = 0; + } + } + else { + proc_info_p->hProcess = 0; + proc_info_p->dwProcessId = 0; + ret = -1; + } + +#else + fl_alert("Process execution only implemented\n" + "for Linux (and similar) and Windows so far..."); + ret = -1; +#endif + +#endif + return(ret); +} + + +// Kill the specified process, if it exists. +// The description is used in error messages + +void +Run::kill_process(const Proc_Info * const proc_info_p, const char * const description) +{ +#ifdef OS_LIKE_UNIX + int exitstatus; + if (*proc_info_p == 0 || waitpid(*proc_info_p, &exitstatus, WNOHANG) + == *proc_info_p) { + // No process spawned or the one we had already died + return; + } + // Not clear how hard to try to kill process. + // SIGTERM should usually work, and is preferable if it works. + // We wait a little while and try SIGKILL if it hasn't died yet. + // We don't check for errors, because the + // only errors that should be possible are bad signal (should be + // impossible, since we hard-code SIGTERM), process doesn't exist + // (already dead, so no need to kill it), or bad permission (we + // spawned it ourself, so ought to have permission to kill it). + (void) kill(*proc_info_p, SIGTERM); + // Wait for up to 3 seconds for it to die + int w; + int ret; + for (w = 0; w < 3; w++) { + if ((ret = waitpid(*proc_info_p, &exitstatus, WNOHANG)) + == *proc_info_p) { + // It died. + // *** Especially in the case of MIDI, there is a + // chance the player has already written stuff out + // to the device driver that it won't clear out when + // it dies, so that if we try to start a new one, + // the device will still be busy. But we have no way + // to know how long to wait, if at all, and don't + // want to always sleep a long time just to cover + // the case of trying to play again while in the middle + // of playing. + return; + } + if (ret == -1 && errno == ECHILD) { + // Child doesn't exist, so nothing to kill + return; + } + sleep(1); + } + // Okay. Resort to SIGKILL and wait 1 more second to let it die. + kill(*proc_info_p, SIGKILL); + sleep(1); +#endif +#ifdef OS_LIKE_WIN32 + if (proc_info_p->hProcess == 0 || WaitForSingleObject( + proc_info_p->hProcess, 0) != WAIT_TIMEOUT) { + // No process spawned or the one we had must have already died. + return; + } + HANDLE handle; + if ( (handle = OpenProcess(PROCESS_TERMINATE, + FALSE, proc_info_p->dwProcessId)) == 0 || + TerminateProcess(handle, 0) == 0) { + // Warn user we were unable to kill the old child. + DWORD format_retval; + LPVOID error_string = 0; + if (FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_ARGUMENT_ARRAY, + 0, GetLastError(), + LANG_NEUTRAL, (LPTSTR)&error_string, + 0, 0)) { + fl_alert("Unable to terminate %s process.\n%s", + description, (char *) error_string); + LocalFree((HLOCAL)error_string); + } + } + if (handle != 0) { + CloseHandle(handle); + } +#endif +} + + +#ifdef OS_LIKE_WIN32 +// Return true if the given command is for Windows Media Player. + +bool +Run::is_mplayer(const char * command) +{ + int length = strlen(command); + if (strcasecmp(command + length - 12, "wmplayer.exe") == 0 || + strcasecmp(command + length - 11, "mplayer.exe") == 0 || + strcasecmp(command + length - 8, "wmplayer") == 0 || + strcasecmp(command + length - 7, "mplayer") == 0) { + return(true); + } + return(false); +} + + +// Return true if the given command is for GSview. + +bool +Run::is_gsview(const char * command) +{ + int length = strlen(command); + if (strcasecmp(command + length - 12, "gsview32.exe") == 0 || + strcasecmp(command + length - 10, "gsview.exe") == 0 || + strcasecmp(command + length - 8, "gsview32") == 0 || + strcasecmp(command + length - 6, "gsview") == 0) { + return(true); + } + return(false); +} +#endif + + +// Called when user begins a new file, or when exiting, +// to kill off child proceses that were spawned. +// We start new processes for new file, +// even if one already up for current file. +// Not sure if that's what user wants, but... + +void +Run::clean_up(void) +{ + kill_process(&display_child, "display"); + kill_process(&MIDI_child, "MIDI player"); +} + + +// Report whether we spawned a display child and think it is still alive. + +bool +Run::has_display_child(void) +{ +#ifdef OS_LIKE_WIN32 + return(display_child.hProcess != 0); +#else + return(display_child != 0); +#endif +} + + +// Report whether we spawned a MIDI child and think it is still alive. + +bool +Run::has_MIDI_child(void) +{ +#ifdef OS_LIKE_WIN32 + return(MIDI_child.hProcess != 0); +#else + return(MIDI_child != 0); +#endif +} + + +//------------ Class for displaying errors from Mup + +Error_report::Error_report(void) + : Fl_Double_Window(Default_width, Default_height, "Error report") +{ + text_p = new Fl_Text_Display(20, 20, w() - 40, h() - 90); + resizable((Fl_Widget *) text_p); + text_p->buffer( new Fl_Text_Buffer() ); + + // Set font/size and arrange to get notified of changes in them + font_change_reg_p = new Font_change_registration( + font_change_cb, (void *) this); + + ok_p = new Fl_Return_Button(w() / 2 - 40, h() - 50, 80, 30, "OK"); + ok_p->callback(OK_cb, this); + show(); + + // Arrange to clean up new-ed widgets in destructor. + end(); +} + +Error_report::~Error_report() +{ +} + + +// Load the error file (from -e of Mup) into window to show user. + +int +Error_report::loadfile(const char * filename) +{ + return text_p->buffer()->loadfile(filename); +} + + +// Callback for when user clicks OK after reading Mup error report + +CALL_BACK(Error_report, OK) +{ + hide(); +} + + +// Callback for change in font/size + +void +Error_report::font_change_cb(void * data, Fl_Font font, unsigned char size) +{ + ((Error_report *)data)->font_change(font, size); +} + +void +Error_report::font_change(Fl_Font font, unsigned char size) +{ + text_p->textfont(font); + text_p->textsize(size); + text_p->redisplay_range(0, text_p->buffer()->length()); +}