aboutsummaryrefslogtreecommitdiff
path: root/test/lib/jdk/test/lib/apps/LingeredApp.java
blob: 22a123ccbb5bfca0568e664dcca395078f0f402a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
/*
 * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.test.lib.apps;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * This is a framework to launch an app that could be synchronized with caller
 * to make further attach actions reliable across supported platforms

 * Caller example:
 *   SmartTestApp a = SmartTestApp.startApp(cmd);
 *     // do something
 *   a.stopApp();
 *
 *   or fine grained control
 *
 *   a = new SmartTestApp("MyLock.lck");
 *   a.createLock();
 *   a.runApp();
 *   a.waitAppReady();
 *     // do something
 *   a.deleteLock();
 *   a.waitAppTerminate();
 *
 *  Then you can work with app output and process object
 *
 *   output = a.getAppOutput();
 *   process = a.getProcess();
 *
 */
public class LingeredApp {

    private static final long spinDelay = 1000;

    private long lockCreationTime;
    private final ArrayList<String> storedAppOutput;

    protected Process appProcess;
    protected static final int appWaitTime = 100;
    protected final String lockFileName;

    /*
     * Drain child process output, store it into string array
     */
    class InputGobbler extends Thread {

        InputStream is;
        List<String> astr;

        InputGobbler(InputStream is, List<String> astr) {
            this.is = is;
            this.astr = astr;
        }

        public void run() {
            try {
                InputStreamReader isr = new InputStreamReader(is);
                BufferedReader br = new BufferedReader(isr);
                String line = null;
                while ((line = br.readLine()) != null) {
                    astr.add(line);
                }
            } catch (IOException ex) {
                // pass
            }
        }
    }

    /**
     * Create LingeredApp object on caller side. Lock file have be a valid filename
     * at writable location
     *
     * @param lockFileName - the name of lock file
     */
    public LingeredApp(String lockFileName) {
        this.lockFileName = lockFileName;
        this.storedAppOutput = new ArrayList<String>();
    }

    public LingeredApp() {
        final String lockName = UUID.randomUUID().toString() + ".lck";
        this.lockFileName = lockName;
        this.storedAppOutput = new ArrayList<String>();
    }

    /**
     *
     * @return name of lock file
     */
    public String getLockFileName() {
        return this.lockFileName;
    }

    /**
     *
     * @return name of testapp
     */
    public String getAppName() {
        return this.getClass().getName();
    }

    /**
     *
     *  @return pid of java process running testapp
     */
    public long getPid() {
        if (appProcess == null) {
            throw new RuntimeException("Process is not alive");
        }
        return appProcess.pid();
    }

    /**
     *
     * @return process object
     */
    public Process getProcess() {
        return appProcess;
    }

    /**
     *
     * @return application output as string array. Empty array if application produced no output
     */
    public List<String> getAppOutput() {
        if (appProcess.isAlive()) {
            throw new RuntimeException("Process is still alive. Can't get its output.");
        }
        return storedAppOutput;
    }

    /* Make sure all part of the app use the same method to get dates,
     as different methods could produce different results
     */
    private static long epoch() {
        return new Date().getTime();
    }

    private static long lastModified(String fileName) throws IOException {
        Path path = Paths.get(fileName);
        BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class);
        return attr.lastModifiedTime().toMillis();
    }

    private static void setLastModified(String fileName, long newTime) throws IOException {
        Path path = Paths.get(fileName);
        FileTime fileTime = FileTime.fromMillis(newTime);
        Files.setLastModifiedTime(path, fileTime);
    }

    /**
     * create lock
     *
     * @throws IOException
     */
    public void createLock() throws IOException {
        Path path = Paths.get(lockFileName);
        // Files.deleteIfExists(path);
        Files.createFile(path);
        lockCreationTime = lastModified(lockFileName);
    }

    /**
     * Delete lock
     *
     * @throws IOException
     */
    public void deleteLock() throws IOException {
        try {
            Path path = Paths.get(lockFileName);
            Files.delete(path);
        } catch (NoSuchFileException ex) {
            // Lock already deleted. Ignore error
        }
    }

    public void waitAppTerminate() {
        while (true) {
            try {
                appProcess.waitFor();
                break;
            } catch (InterruptedException ex) {
                // pass
            }
        }
    }

    /**
     * The app touches the lock file when it's started
     * wait while it happens. Caller have to delete lock on wait error.
     *
     * @param timeout
     * @throws java.io.IOException
     */
    public void waitAppReady(long timeout) throws IOException {
        long here = epoch();
        while (true) {
            long epoch = epoch();
            if (epoch - here > (timeout * 1000)) {
                throw new IOException("App waiting timeout");
            }

            // Live process should touch lock file every second
            long lm = lastModified(lockFileName);
            if (lm > lockCreationTime) {
                break;
            }

            // Make sure process didn't already exit
            if (!appProcess.isAlive()) {
                throw new IOException("App exited unexpectedly with " + appProcess.exitValue());
            }

            try {
                Thread.sleep(spinDelay);
            } catch (InterruptedException ex) {
                // pass
            }
        }
    }

    /**
     * Analyze an environment and prepare a command line to
     * run the app, app name should be added explicitly
     */
    public List<String> runAppPrepare(List<String> vmArguments) {
        // We should always use testjava or throw an exception,
        // so we can't use JDKToolFinder.getJDKTool("java");
        // that falls back to compile java on error
        String jdkPath = System.getProperty("test.jdk");
        if (jdkPath == null) {
            // we are not under jtreg, try env
            Map<String, String> env = System.getenv();
            jdkPath = env.get("TESTJAVA");
        }

        if (jdkPath == null) {
            throw new RuntimeException("Can't determine jdk path neither test.jdk property no TESTJAVA env are set");
        }

        String osname = System.getProperty("os.name");
        String javapath = jdkPath + ((osname.startsWith("window")) ? "/bin/java.exe" : "/bin/java");

        List<String> cmd = new ArrayList<String>();
        cmd.add(javapath);


        if (vmArguments == null) {
            // Propagate test.vm.options to LingeredApp, filter out possible empty options
            String testVmOpts[] = System.getProperty("test.vm.opts","").split("\\s+");
            for (String s : testVmOpts) {
                if (!s.equals("")) {
                    cmd.add(s);
                }
            }
        }
        else{
            // Lets user manage LingeredApp options
            cmd.addAll(vmArguments);
        }

        // Make sure we set correct classpath to run the app
        cmd.add("-cp");
        String classpath = System.getProperty("test.class.path");
        cmd.add((classpath == null) ? "." : classpath);

        return cmd;
    }

    /**
     * Assemble command line to a printable string
     */
    public void printCommandLine(List<String> cmd) {
        // A bit of verbosity
        StringBuilder cmdLine = new StringBuilder();
        for (String strCmd : cmd) {
            cmdLine.append("'").append(strCmd).append("' ");
        }

        System.out.println("Command line: [" + cmdLine.toString() + "]");
    }

    public void startGobblerPipe() {
      // Create pipe reader for process, and read stdin and stderr to array of strings
      InputGobbler gb = new InputGobbler(appProcess.getInputStream(), storedAppOutput);
      gb.start();
    }

    /**
     * Run the app.
     *
     * @param vmArguments
     * @throws IOException
     */
    public void runApp(List<String> vmArguments)
            throws IOException {

        List<String> cmd = runAppPrepare(vmArguments);

        cmd.add(this.getAppName());
        cmd.add(lockFileName);

        printCommandLine(cmd);

        ProcessBuilder pb = new ProcessBuilder(cmd);
        // we don't expect any error output but make sure we are not stuck on pipe
        // pb.redirectErrorStream(false);
        // ProcessBuilder.start can throw IOException
        pb.redirectError(ProcessBuilder.Redirect.INHERIT);
        appProcess = pb.start();

        startGobblerPipe();
    }

    /**
     * Delete lock file that signals app to terminate, then
     * wait until app is actually terminated.
     * @throws IOException
     */
    public void stopApp() throws IOException {
        deleteLock();
        // The startApp() of the derived app can throw
        // an exception before the LA actually starts
        if (appProcess != null) {
            waitAppTerminate();
            int exitcode = appProcess.exitValue();
            if (exitcode != 0) {
                throw new IOException("LingeredApp terminated with non-zero exit code " + exitcode);
            }
        }
    }

    /**
     *  High level interface for test writers
     */
    /**
     * Factory method that creates LingeredApp object with ready to use application
     * lock name is autogenerated
     * @param cmd - vm options, could be null to auto add testvm.options
     * @return LingeredApp object
     * @throws IOException
     */
    public static LingeredApp startApp(List<String> cmd) throws IOException {
        LingeredApp a = new LingeredApp();
        a.createLock();
        try {
            a.runApp(cmd);
            a.waitAppReady(appWaitTime);
        } catch (Exception ex) {
            a.deleteLock();
            throw ex;
        }

        return a;
    }

    /**
     * Factory method that starts pre-created LingeredApp
     * lock name is autogenerated
     * @param cmd - vm options, could be null to auto add testvm.options
     * @param theApp - app to start
     * @return LingeredApp object
     * @throws IOException
     */

    public static void startApp(List<String> cmd, LingeredApp theApp) throws IOException {
        theApp.createLock();
        try {
            theApp.runApp(cmd);
            theApp.waitAppReady(appWaitTime);
        } catch (Exception ex) {
            theApp.deleteLock();
            throw ex;
        }
    }

    public static LingeredApp startApp() throws IOException {
        return startApp(null);
    }

    public static void stopApp(LingeredApp app) throws IOException {
        if (app != null) {
            // LingeredApp can throw an exception during the intialization,
            // make sure we don't have cascade NPE
            app.stopApp();
        }
    }

    /**
     * LastModified time might not work correctly in some cases it might
     * cause later failures
     */

    public static boolean isLastModifiedWorking() {
        boolean sane = true;
        try {
            long lm = lastModified(".");
            if (lm == 0) {
                System.err.println("SANITY Warning! The lastModifiedTime() doesn't work on this system, it returns 0");
                sane = false;
            }

            long now = epoch();
            if (lm > now) {
                System.err.println("SANITY Warning! The Clock is wrong on this system lastModifiedTime() > getTime()");
                sane = false;
            }

            setLastModified(".", epoch());
            long lm1 = lastModified(".");
            if (lm1 <= lm) {
                System.err.println("SANITY Warning! The setLastModified doesn't work on this system");
                sane = false;
            }
        }
        catch(IOException e) {
            System.err.println("SANITY Warning! IOException during sanity check " + e);
            sane = false;
        }

        return sane;
    }

    /**
     * This part is the application it self
     */
    public static void main(String args[]) {

        if (args.length != 1) {
            System.err.println("Lock file name is not specified");
            System.exit(7);
        }

        String theLockFileName = args[0];

        try {
            Path path = Paths.get(theLockFileName);

            while (Files.exists(path)) {
                // Touch the lock to indicate our readiness
                setLastModified(theLockFileName, epoch());
                Thread.sleep(spinDelay);
            }
        } catch (NoSuchFileException ex) {
            // Lock deleted while we are setting last modified time.
            // Ignore error and lets the app exits
        } catch (Exception ex) {
            System.err.println("LingeredApp ERROR: " + ex);
            // Leave exit_code = 1 to Java launcher
            System.exit(3);
        }

        System.exit(0);
    }
}