001/*
002 * IzPack - Copyright 2001-2005 Julien Ponge, All Rights Reserved.
003 * 
004 * http://www.izforge.com/izpack/
005 * http://developer.berlios.de/projects/izpack/
006 * 
007 * Copyright 2004 Tino Schwarze
008 * 
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *     http://www.apache.org/licenses/LICENSE-2.0
014 *     
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 */
021
022package com.izforge.izpack.installer;
023
024import java.io.BufferedReader;
025import java.io.File;
026import java.io.FileOutputStream;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.InputStreamReader;
030import java.io.PrintWriter;
031import java.lang.reflect.InvocationTargetException;
032import java.lang.reflect.Method;
033import java.text.SimpleDateFormat;
034import java.util.ArrayList;
035import java.util.Date;
036import java.util.Iterator;
037import java.util.List;
038import java.util.Vector;
039
040import net.n3.nanoxml.NonValidator;
041import net.n3.nanoxml.StdXMLBuilder;
042import net.n3.nanoxml.StdXMLParser;
043import net.n3.nanoxml.StdXMLReader;
044import net.n3.nanoxml.XMLElement;
045
046import com.izforge.izpack.Pack;
047import com.izforge.izpack.util.AbstractUIHandler;
048import com.izforge.izpack.util.AbstractUIProcessHandler;
049import com.izforge.izpack.util.Debug;
050import com.izforge.izpack.util.IoHelper;
051import com.izforge.izpack.util.OsConstraint;
052import com.izforge.izpack.util.VariableSubstitutor;
053
054/**
055 * This class does alle the work for the process panel.
056 * 
057 * It responsible for
058 * <ul>
059 * <li>parsing the process spec XML file
060 * <li>performing the actions described therein
061 * </ul>
062 * 
063 * @author Tino Schwarze
064 */
065public class ProcessPanelWorker implements Runnable
066{
067
068    /** Name of resource for specifying processing parameters. */
069    private static final String SPEC_RESOURCE_NAME = "ProcessPanel.Spec.xml";
070
071    private VariableSubstitutor vs;
072
073    private XMLElement spec;
074
075    protected AbstractUIProcessHandler handler;
076
077    private ArrayList jobs = new ArrayList();
078
079    private Thread processingThread = null;
080
081    private static PrintWriter logfile = null;
082
083    private String logfiledir = null;
084
085    protected AutomatedInstallData idata;
086
087    /**
088     * The constructor.
089     * 
090     * @param idata The installation data.
091     * @param handler The handler to notify of progress.
092     */
093    public ProcessPanelWorker(AutomatedInstallData idata, AbstractUIProcessHandler handler)
094            throws IOException
095    {
096        this.handler = handler;
097        this.idata = idata;
098        this.vs = new VariableSubstitutor(idata.getVariables());
099
100        // Removed this test in order to move out of the CTOR (ExecuteForPack
101        // Patch)
102        // if (!readSpec())
103        // throw new IOException("Error reading processing specification");
104    }
105
106    private boolean readSpec() throws IOException
107    {
108        InputStream input;
109        try
110        {
111            input = ResourceManager.getInstance().getInputStream(SPEC_RESOURCE_NAME);
112        }
113        catch (Exception e)
114        {
115            e.printStackTrace();
116            return false;
117        }
118
119        StdXMLParser parser = new StdXMLParser();
120        parser.setBuilder(new StdXMLBuilder());
121        parser.setValidator(new NonValidator());
122
123        try
124        {
125            parser.setReader(new StdXMLReader(input));
126
127            this.spec = (XMLElement) parser.parse();
128        }
129        catch (Exception e)
130        {
131            System.err.println("Error parsing XML specification for processing.");
132            System.err.println(e.toString());
133            return false;
134        }
135
136        if (!this.spec.hasChildren()) return false;
137
138        // Handle logfile
139        XMLElement lfd = spec.getFirstChildNamed("logfiledir");
140        if (lfd != null)
141        {
142            logfiledir = lfd.getContent();
143        }
144
145        for (Iterator job_it = this.spec.getChildrenNamed("job").iterator(); job_it.hasNext();)
146        {
147            XMLElement job_el = (XMLElement) job_it.next();
148
149            // ExecuteForPack Patch
150            // Check if processing required for pack
151            Vector forPacks = job_el.getChildrenNamed("executeForPack");
152            if (!jobRequiredFor(forPacks))
153            {
154                continue;
155            }
156
157            // first check OS constraints - skip jobs not suited for this OS
158            List constraints = OsConstraint.getOsList(job_el);
159
160            if (OsConstraint.oneMatchesCurrentSystem(constraints))
161            {
162                List ef_list = new ArrayList();
163
164                String job_name = job_el.getAttribute("name", "");
165
166                for (Iterator ef_it = job_el.getChildrenNamed("executefile").iterator(); ef_it
167                        .hasNext();)
168                {
169                    XMLElement ef = (XMLElement) ef_it.next();
170
171                    String ef_name = ef.getAttribute("name");
172
173                    if ((ef_name == null) || (ef_name.length() == 0))
174                    {
175                        System.err.println("missing \"name\" attribute for <executefile>");
176                        return false;
177                    }
178
179                    List args = new ArrayList();
180
181                    for (Iterator arg_it = ef.getChildrenNamed("arg").iterator(); arg_it.hasNext();)
182                    {
183                        XMLElement arg_el = (XMLElement) arg_it.next();
184
185                        String arg_val = arg_el.getContent();
186
187                        args.add(arg_val);
188                    }
189
190                    ef_list.add(new ExecutableFile(ef_name, args));
191                }
192
193                for (Iterator ef_it = job_el.getChildrenNamed("executeclass").iterator(); ef_it
194                        .hasNext();)
195                {
196                    XMLElement ef = (XMLElement) ef_it.next();
197                    String ef_name = ef.getAttribute("name");
198                    if ((ef_name == null) || (ef_name.length() == 0))
199                    {
200                        System.err.println("missing \"name\" attribute for <executeclass>");
201                        return false;
202                    }
203
204                    List args = new ArrayList();
205                    for (Iterator arg_it = ef.getChildrenNamed("arg").iterator(); arg_it.hasNext();)
206                    {
207                        XMLElement arg_el = (XMLElement) arg_it.next();
208                        String arg_val = arg_el.getContent();
209                        args.add(arg_val);
210                    }
211
212                    ef_list.add(new ExecutableClass(ef_name, args));
213                }
214                this.jobs.add(new ProcessingJob(job_name, ef_list));
215            }
216
217        }
218
219        return true;
220    }
221
222    /**
223     * This is called when the processing thread is activated.
224     * 
225     * Can also be called directly if asynchronous processing is not desired.
226     */
227    public void run()
228    {
229        // ExecuteForPack patch
230        // Read spec only here... not before, cause packs are otherwise
231        // all selected or de-selected
232        try
233        {
234            if (!readSpec())
235            {
236                System.err.println("Error parsing XML specification for processing.");
237                return;
238            }
239        }
240        catch (java.io.IOException ioe)
241        {
242            System.err.println(ioe.toString());
243            return;
244        }
245
246        // Create logfile if needed. Do it at this point because
247        // variable substitution needs selected install path.
248        if (logfiledir != null)
249        {
250            logfiledir = IoHelper.translatePath(logfiledir, new VariableSubstitutor(idata
251                    .getVariables()));
252
253            File lf;
254
255            String appVersion = idata.getVariable("APP_VER");
256
257            if (appVersion != null)
258                appVersion = "V" + appVersion;
259            else
260                appVersion = "undef";
261
262            String identifier = (new SimpleDateFormat("yyyyMMddHHmmss")).format(new Date());
263
264            identifier = appVersion.replace(' ', '_') + "_" + identifier;
265
266            try
267            {
268                lf = File.createTempFile("Install_" + identifier + "_", ".log",
269                        new File(logfiledir));
270                logfile = new PrintWriter(new FileOutputStream(lf), true);
271            }
272            catch (IOException e)
273            {
274                Debug.error(e);
275                // TODO throw or throw not, that's the question...
276            }
277        }
278
279        this.handler.startProcessing(this.jobs.size());
280
281        for (Iterator job_it = this.jobs.iterator(); job_it.hasNext();)
282        {
283            ProcessingJob pj = (ProcessingJob) job_it.next();
284
285            this.handler.startProcess(pj.name);
286
287            boolean result = pj.run(this.handler, this.vs);
288
289            this.handler.finishProcess();
290
291            if (!result) break;
292        }
293
294        this.handler.finishProcessing();
295        if (logfile != null) logfile.close();
296    }
297
298    /** Start the compilation in a separate thread. */
299    public void startThread()
300    {
301        this.processingThread = new Thread(this, "processing thread");
302        // will call this.run()
303        this.processingThread.start();
304    }
305
306    interface Processable
307    {
308
309        /**
310         * @param handler The UI handler for user interaction and to send output to.
311         * @return true on success, false if processing should stop
312         */
313        public boolean run(AbstractUIProcessHandler handler, VariableSubstitutor vs);
314    }
315
316    private static class ProcessingJob implements Processable
317    {
318
319        public String name;
320
321        private List processables;
322
323        public ProcessingJob(String name, List processables)
324        {
325            this.name = name;
326            this.processables = processables;
327        }
328
329        public boolean run(AbstractUIProcessHandler handler, VariableSubstitutor vs)
330        {
331            for (Iterator pr_it = this.processables.iterator(); pr_it.hasNext();)
332            {
333                Processable pr = (Processable) pr_it.next();
334
335                if (!pr.run(handler, vs)) return false;
336            }
337
338            return true;
339        }
340
341    }
342
343    private static class ExecutableFile implements Processable
344    {
345
346        private String filename;
347
348        private List arguments;
349
350        protected AbstractUIProcessHandler handler;
351
352        public ExecutableFile(String fn, List args)
353        {
354            this.filename = fn;
355            this.arguments = args;
356        }
357
358        public boolean run(AbstractUIProcessHandler handler, VariableSubstitutor vs)
359        {
360            this.handler = handler;
361
362            String params[] = new String[this.arguments.size() + 1];
363
364            params[0] = vs.substitute(this.filename, "plain");
365
366            int i = 1;
367            for (Iterator arg_it = this.arguments.iterator(); arg_it.hasNext();)
368            {
369                params[i++] = vs.substitute((String) arg_it.next(), "plain");
370            }
371
372            try
373            {
374                Process p = Runtime.getRuntime().exec(params);
375
376                OutputMonitor stdoutMon = new OutputMonitor(this.handler, p.getInputStream(), false);
377                OutputMonitor stderrMon = new OutputMonitor(this.handler, p.getErrorStream(), true);
378                Thread stdoutThread = new Thread(stdoutMon);
379                Thread stderrThread = new Thread(stderrMon);
380                stdoutThread.setDaemon(true);
381                stderrThread.setDaemon(true);
382                stdoutThread.start();
383                stderrThread.start();
384
385                try
386                {
387                    int exitStatus = p.waitFor();
388
389                    stopMonitor(stdoutMon, stdoutThread);
390                    stopMonitor(stderrMon, stderrThread);
391
392                    if (exitStatus != 0)
393                    {
394                        if (this.handler.askQuestion("process execution failed",
395                                "Continue anyway?", AbstractUIHandler.CHOICES_YES_NO,
396                                AbstractUIHandler.ANSWER_YES) == AbstractUIHandler.ANSWER_NO) { return false; }
397                    }
398                }
399                catch (InterruptedException ie)
400                {
401                    p.destroy();
402                    this.handler.emitError("process interrupted", ie.toString());
403                    return false;
404                }
405            }
406            catch (IOException ioe)
407            {
408                this.handler.emitError("I/O error", ioe.toString());
409                return false;
410            }
411
412            return true;
413        }
414
415        private void stopMonitor(OutputMonitor m, Thread t)
416        {
417            // taken from com.izforge.izpack.util.FileExecutor
418            m.doStop();
419            long softTimeout = 500;
420            try
421            {
422                t.join(softTimeout);
423            }
424            catch (InterruptedException e)
425            {}
426
427            if (t.isAlive() == false) return;
428
429            t.interrupt();
430            long hardTimeout = 500;
431            try
432            {
433                t.join(hardTimeout);
434            }
435            catch (InterruptedException e)
436            {}
437        }
438
439        static public class OutputMonitor implements Runnable
440        {
441
442            private boolean stderr = false;
443
444            private AbstractUIProcessHandler handler;
445
446            private BufferedReader reader;
447
448            private Boolean stop = Boolean.valueOf(false);
449
450            public OutputMonitor(AbstractUIProcessHandler handler, InputStream is, boolean stderr)
451            {
452                this.stderr = stderr;
453                this.reader = new BufferedReader(new InputStreamReader(is));
454                this.handler = handler;
455            }
456
457            public void run()
458            {
459                try
460                {
461                    String line;
462                    while ((line = reader.readLine()) != null)
463                    {
464                        this.handler.logOutput(line, stderr);
465
466                        // log output also to file given in ProcessPanelSpec
467
468                        if (logfile != null) logfile.println(line);
469
470                        synchronized (this.stop)
471                        {
472                            if (stop.booleanValue()) return;
473                        }
474                    }
475                }
476                catch (IOException ioe)
477                {
478                    this.handler.logOutput(ioe.toString(), true);
479
480                    // log errors also to file given in ProcessPanelSpec
481
482                    if (logfile != null) logfile.println(ioe.toString());
483
484                }
485
486            }
487
488            public void doStop()
489            {
490                synchronized (this.stop)
491                {
492                    this.stop = Boolean.valueOf(true);
493                }
494            }
495
496        }
497
498    }
499
500    /**
501     * Tries to create a class that has an empty contstructor and a method
502     * run(AbstractUIProcessHandler, String[]) If found, it calls the method and processes all
503     * returned exceptions
504     */
505    private static class ExecutableClass implements Processable
506    {
507
508        final private String myClassName;
509
510        final private List myArguments;
511
512        protected AbstractUIProcessHandler myHandler;
513
514        public ExecutableClass(String className, List args)
515        {
516            myClassName = className;
517            myArguments = args;
518        }
519
520        public boolean run(AbstractUIProcessHandler aHandler, VariableSubstitutor varSubstitutor)
521        {
522            boolean result = false;
523            myHandler = aHandler;
524
525            String params[] = new String[myArguments.size()];
526
527            int i = 0;
528            for (Iterator arg_it = myArguments.iterator(); arg_it.hasNext();)
529                params[i++] = varSubstitutor.substitute((String) arg_it.next(), "plain");
530
531            try
532            {
533                ClassLoader loader = this.getClass().getClassLoader();
534                Class procClass = loader.loadClass(myClassName);
535
536                Object o = procClass.newInstance();
537                Method m = procClass.getMethod("run", new Class[] { AbstractUIProcessHandler.class,
538                        String[].class});
539
540                m.invoke(o, new Object[] { myHandler, params});
541                result = true;
542            }
543            catch (SecurityException e)
544            {
545                myHandler.emitError("Post Processing Error",
546                        "Security exception thrown when processing class: " + myClassName);
547            }
548            catch (ClassNotFoundException e)
549            {
550                myHandler.emitError("Post Processing Error", "Cannot find processing class: "
551                        + myClassName);
552            }
553            catch (NoSuchMethodException e)
554            {
555                myHandler.emitError("Post Processing Error",
556                        "Processing class does not have 'run' method: " + myClassName);
557            }
558            catch (IllegalAccessException e)
559            {
560                myHandler.emitError("Post Processing Error", "Error accessing processing class: "
561                        + myClassName);
562            }
563            catch (InvocationTargetException e)
564            {
565                myHandler.emitError("Post Processing Error", "Invocation Problem calling : "
566                        + myClassName + ", " + e.getCause().getMessage());
567            }
568            catch (Exception e)
569            {
570                myHandler.emitError("Post Processing Error",
571                        "Exception when running processing class: " + myClassName + ", "
572                                + e.getMessage());
573            }
574            catch (Error e)
575            {
576                myHandler.emitError("Post Processing Error",
577                        "Error when running processing class: " + myClassName + ", "
578                                + e.getMessage());
579            }
580            catch (Throwable e)
581            {
582                myHandler.emitError("Post Processing Error",
583                        "Error when running processing class: " + myClassName + ", "
584                                + e.getMessage());
585            }
586            return result;
587        }
588    }
589
590    /*------------------------ ExecuteForPack PATCH -------------------------*/
591    /*
592     * Verifies if the job is required for any of the packs listed. The job is required for a pack
593     * in the list if that pack is actually selected for installation. <br><br> <b>Note:</b><br>
594     * If the list of selected packs is empty then <code>true</code> is always returned. The same
595     * is true if the <code>packs</code> list is empty.
596     * 
597     * @param packs a <code>Vector</code> of <code>String</code>s. Each of the strings denotes
598     * a pack for which the schortcut should be created if the pack is actually installed.
599     * 
600     * @return <code>true</code> if the shortcut is required for at least on pack in the list,
601     * otherwise returns <code>false</code>.
602     */
603    /*--------------------------------------------------------------------------*/
604    /*
605     * @design
606     * 
607     * The information about the installed packs comes from InstallData.selectedPacks. This assumes
608     * that this panel is presented to the user AFTER the PacksPanel.
609     * 
610     * /*--------------------------------------------------------------------------
611     */
612
613    private boolean jobRequiredFor(Vector packs)
614    {
615        String selected;
616        String required;
617
618        if (packs.size() == 0) { return (true); }
619
620        // System.out.println ("Number of selected packs is "
621        // +idata.selectedPacks.size () );
622
623        for (int i = 0; i < idata.selectedPacks.size(); i++)
624        {
625            selected = ((Pack) idata.selectedPacks.get(i)).name;
626
627            // System.out.println ("Selected pack is " + selected);
628
629            for (int k = 0; k < packs.size(); k++)
630            {
631                required = (String) ((XMLElement) packs.elementAt(k)).getAttribute("name", "");
632                // System.out.println ("Attribute name is " + required);
633                if (selected.equals(required))
634                {
635                    // System.out.println ("Return true");
636                    return (true);
637                }
638            }
639        }
640        return (false);
641    }
642
643}