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 2002 Olexij Tkatchenko
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.util;
023
024import java.io.BufferedReader;
025import java.io.BufferedWriter;
026import java.io.File;
027import java.io.IOException;
028import java.io.InputStreamReader;
029import java.io.Reader;
030import java.io.StringWriter;
031import java.io.Writer;
032import java.util.ArrayList;
033import java.util.Collection;
034import java.util.Iterator;
035import java.util.List;
036
037import com.izforge.izpack.ExecutableFile;
038
039/**
040 * Executes a bunch of files. This class is intended to do a system dependent installation
041 * postprocessing. Executable file can be any file installed with current package. After execution
042 * the file can be optionally removed. Before execution on Unix systems execution flag will be set
043 * on processed file.
044 * 
045 * @author Olexij Tkatchenko <ot@parcs.de>
046 */
047public class FileExecutor
048{
049
050    /**
051     * This is a grabber for stdout and stderr. It will be launched once at command execution end
052     * terminates if the apropriate stream runs out of data.
053     * 
054     * @author Olexij Tkatchenko <ot@parcs.de>
055     */
056    private static class MonitorInputStream implements Runnable
057    {
058
059        private BufferedReader reader;
060
061        private BufferedWriter writer;
062
063        private boolean shouldStop = false;
064
065        public MonitorInputStream(Reader in, Writer out)
066        {
067            reader = new BufferedReader(in);
068            writer = new BufferedWriter(out);
069        }
070
071        public void doStop()
072        {
073            shouldStop = true;
074        }
075
076        public void run()
077        {
078            try
079            {
080                String line;
081                while ((line = reader.readLine()) != null)
082                {
083                    writer.write(line);
084                    writer.newLine();
085                    writer.flush();
086                    if (shouldStop) return;
087                }
088            }
089            catch (IOException ioe)
090            {
091                ioe.printStackTrace(System.out);
092            }
093        }
094    }
095
096    private boolean stopThread(Thread t, MonitorInputStream m)
097    {
098        m.doStop();
099        long softTimeout = 1000;
100        try
101        {
102            t.join(softTimeout);
103        }
104        catch (InterruptedException e)
105        {}
106
107        if (!t.isAlive()) return true;
108
109        t.interrupt();
110        long hardTimeout = 1000;
111        try
112        {
113            t.join(hardTimeout);
114        }
115        catch (InterruptedException e)
116        {}
117        return !t.isAlive();
118    }
119
120    /**
121     * Constructs a new executor. The executable files specified must have pretranslated paths
122     * (variables expanded and file separator characters converted if necessary).
123     * 
124     * @param files the executable files to process
125     */
126    public FileExecutor(Collection files)
127    {
128        this.files = files;
129    }
130
131    /**
132     * Constructs a new executor.
133     */
134    public FileExecutor()
135    {
136        this.files = null;
137    }
138
139    /**
140     * Executed a system command and waits for completion.
141     * 
142     * @param params system command as string array
143     * @param output contains output of the command index 0 = standard output index 1 = standard
144     * error
145     * @return exit status of process
146     */
147    public int executeCommand(String[] params, String[] output)
148    {
149        StringBuffer retval = new StringBuffer();
150        retval.append("executeCommand\n");
151        if (params != null)
152        {
153            for (int i = 0; i < params.length; i++)
154            {
155                retval.append("\tparams: " + params[i]);
156                retval.append("\n");
157            }
158        }
159        Process process = null;
160        MonitorInputStream outMonitor = null;
161        MonitorInputStream errMonitor = null;
162        Thread t1 = null;
163        Thread t2 = null;
164        int exitStatus = -1;
165
166        Debug.trace(retval);
167
168        try
169        {
170            // execute command
171            process = Runtime.getRuntime().exec(params);
172
173            boolean console = false;// TODO: impl from xml <execute
174            // in_console=true ...>, but works already
175            // if this flag is true
176            if (console)
177            {
178                Console c = new Console(process);
179                // save command output
180                output[0] = c.getOutputData();
181                output[1] = c.getErrorData();
182                exitStatus = process.exitValue();
183            }
184            else
185            {
186                StringWriter outWriter = new StringWriter();
187                StringWriter errWriter = new StringWriter();
188
189                InputStreamReader or = new InputStreamReader(process.getInputStream());
190                InputStreamReader er = new InputStreamReader(process.getErrorStream());
191                outMonitor = new MonitorInputStream(or, outWriter);
192                errMonitor = new MonitorInputStream(er, errWriter);
193                t1 = new Thread(outMonitor);
194                t2 = new Thread(errMonitor);
195                t1.setDaemon(true);
196                t2.setDaemon(true);
197                t1.start();
198                t2.start();
199
200                // wait for command to complete
201                exitStatus = process.waitFor();
202                t1.join();
203                t2.join();
204
205                // save command output
206                output[0] = outWriter.toString();
207                Debug.trace("stdout:");
208                Debug.trace(output[0]);
209                output[1] = errWriter.toString();
210                Debug.trace("stderr:");
211                Debug.trace(output[1]);
212            }
213            Debug.trace("exit status: " + Integer.toString(exitStatus));
214        }
215        catch (InterruptedException e)
216        {
217            if (Debug.tracing()) e.printStackTrace(System.err);
218            stopThread(t1, outMonitor);
219            stopThread(t2, errMonitor);
220            output[0] = "";
221            output[1] = e.getMessage() + "\n";
222            process.destroy();
223        }
224        catch (IOException e)
225        {
226            if (Debug.tracing()) e.printStackTrace(System.err);
227            output[0] = "";
228            output[1] = e.getMessage() + "\n";
229        }
230        return exitStatus;
231    }
232
233    /**
234     * Executes files specified at construction time.
235     * 
236     * @param currentStage the stage of the installation
237     * @param handler The AbstractUIHandler to notify on errors.
238     * 
239     * @return 0 on success, else the exit status of the last failed command
240     */
241    public int executeFiles(int currentStage, AbstractUIHandler handler)
242    {
243        int exitStatus = 0;
244        String[] output = new String[2];
245        // String permissions = (System.getProperty("user.name").equals("root"))
246        // ? "a+x" : "u+x";
247        String permissions = "a+x";
248
249        // loop through all executables
250        Iterator efileIterator = files.iterator();
251        while (exitStatus == 0 && efileIterator.hasNext())
252        {
253            ExecutableFile efile = (ExecutableFile) efileIterator.next();
254            boolean deleteAfterwards = !efile.keepFile;
255            File file = new File(efile.path);
256            Debug.trace("handeling executable file " + efile);
257
258            // skip file if not for current OS (it might not have been installed
259            // at all)
260            if (!OsConstraint.oneMatchesCurrentSystem(efile.osList)) continue;
261
262            if (currentStage != ExecutableFile.UNINSTALL && OsVersion.IS_UNIX)
263            {
264                // fix executable permission for unix systems
265                Debug.trace("making file executable (setting executable flag)");
266                String[] params = { "/bin/chmod", permissions, file.toString()};
267                exitStatus = executeCommand(params, output);
268                if (exitStatus != 0)
269                {
270                    handler.emitError("file execution error", "Error executing \n" + params[0]
271                            + " " + params[1] + " " + params[2]);
272                    continue;
273                }
274            }
275
276            // execute command in POSTINSTALL stage
277            if ((exitStatus == 0)
278                    && ((currentStage == ExecutableFile.POSTINSTALL && efile.executionStage == ExecutableFile.POSTINSTALL) || (currentStage == ExecutableFile.UNINSTALL && efile.executionStage == ExecutableFile.UNINSTALL)))
279            {
280                List paramList = new ArrayList();
281                if (ExecutableFile.BIN == efile.type)
282                    paramList.add(file.toString());
283
284                else if (ExecutableFile.JAR == efile.type && null == efile.mainClass)
285                {
286                    paramList.add(System.getProperty("java.home") + "/bin/java");
287                    paramList.add("-jar");
288                    paramList.add(file.toString());
289                }
290                else if (ExecutableFile.JAR == efile.type && null != efile.mainClass)
291                {
292                    paramList.add(System.getProperty("java.home") + "/bin/java");
293                    paramList.add("-cp");
294                    paramList.add(file.toString());
295                    paramList.add(efile.mainClass);
296                }
297
298                if (null != efile.argList && !efile.argList.isEmpty())
299                    paramList.addAll(efile.argList);
300
301                String[] params = new String[paramList.size()];
302                for (int i = 0; i < paramList.size(); i++)
303                    params[i] = (String) paramList.get(i);
304
305                exitStatus = executeCommand(params, output);
306
307                // bring a dialog depending on return code and failure handling
308                if (exitStatus != 0)
309                {
310                    deleteAfterwards = false;
311                    String message = output[0] + "\n" + output[1];
312                    if (message.length() == 1)
313                        message = "Failed to execute " + file.toString() + ".";
314
315                    if (efile.onFailure == ExecutableFile.ABORT)
316                    {
317                        // CHECKME: let the user decide or abort anyway?
318                        handler.emitError("file execution error", message);
319                    }
320                    else if (efile.onFailure == ExecutableFile.WARN)
321                    {
322                        // CHECKME: let the user decide or abort anyway?
323                        handler.emitWarning("file execution error", message);
324                        exitStatus = 0;
325                    }
326                    else
327                    {
328                        if (handler
329                                .askQuestion(null, "Continue?", AbstractUIHandler.CHOICES_YES_NO) == AbstractUIHandler.ANSWER_YES)
330                            exitStatus = 0;
331                    }
332
333                }
334
335            }
336
337            // POSTINSTALL executables will be deleted
338            if (efile.executionStage == ExecutableFile.POSTINSTALL && deleteAfterwards)
339            {
340                if (file.canWrite()) file.delete();
341            }
342
343        }
344        return exitStatus;
345    }
346
347    /** The files to execute. */
348    private Collection files;
349}