Определение того, была ли запущена программа Java из интерактивной оболочки

Раньше я думал

System.console() != null

был надежным способом определить, является ли оболочка, запускающая мое приложение Java, интерактивной или нет. Это позволило мне использовать ANSI escape-последовательности в интерактивном режиме и обычный System.out/System.err всякий раз, когда выход программы был перенаправлен в файл или подключен к модулю какого-либо другого процесса, подобно режиму --color=auto многих утилит GNU.

Однако поведение

System.console() отличается от Windows. Хотя метод возвращает значение не null, когда JVM запускается из cmd.exe (что для меня бесполезно, поскольку cmd.exe не понимает escape-последовательности), возвращаемое значение всегда null, когда я запускаю свою программу из любого из эмуляторов терминала, доступных в Cygwin - xterm, mintty или cygwin (последний - это просто cmd.exe, выполняющий дочерний процесс bash)..

Как проверить интерактивную оболочку в Java без чтения $- в сценариях оболочки и передать аргументы командной строки моей программе Java? Тестирование переменной PS1 окружения из Java не является вариантом, так как Java запускается из оболочки script, поэтому родительский процесс является неинтерактивной оболочкой, а PS1 не задан.

Ответ 1

Существует беседа , где сторонник Cygwin (Corinna Vinschen) объясняет, что псевдотемы TTY Cygwin выглядят как трубки в Microsoft Visual C во время выполнения библиотеки (MSVCRT). Она также предлагает внедрить обертку вокруг функции isatty(), которая распознает псевдонимы Tugs Cygwin.

Идея состоит в том, чтобы получить имя канала, связанного с данным файловым дескриптором. Функция NtQueryInformationFile извлекает FILE_NAME_INFORMATION, где Элемент FileName содержит имя трубы. Если имя трубы соответствует следующему шаблону, то очень вероятно, что команда запущена в интерактивном режиме:

\cygwin-%16llx-pty%d-{to,from}-master

Разговор довольно старый, но формат имен труб остается прежним: "\\\\.\\pipe\\cygwin-" + "%S-" + + "pty%d-from-master", где "\\\\.\\pipe\\" является префиксом для именованных каналов (см. CreateNamedPipe).

Итак, часть Cygwin уже взломана. Следующий шаг - сделать функцию Java из кода C.

Пример

Далее создается класс ttyjni.TestApp с методом istty(), реализованный через интерфейс Java Native Interface (JNI). Код тестируется на GNU/Linux (x86_64) и Cygwin на Windows 7 (64-разрядная версия). Код можно легко портировать в Windows (cmd.exe), возможно, даже работает так, как есть.

Необходимые компоненты

  • Cygwin с компилятором x86_64-w64-mingw32-gcc
  • Windows с JDK

Разметка

├── Makefile
├── TestApp.c
├── test.sh
├── ttyjni
│   └── TestApp.java
└── ttyjni_TestApp.h

Makefile

# Input: $JAVA_HOME

FINAL_TARGETS := TestApp.class

ifeq ($(OS),Windows_NT)
  CC=x86_64-w64-mingw32-gcc
  FINAL_TARGETS += testapp.dll
else
  CC=gcc
  FINAL_TARGETS += libtestapp.so
endif

all: $(FINAL_TARGETS)

TestApp.class: ttyjni/TestApp.java
  javac $<

testapp.dll: TestApp.c TestApp.class
  $(CC) \
    -Wl,--add-stdcall-alias \
    -D__int64="long long" \
    -D_isatty=isatty -D_fileno=fileno \
    -I"$(JAVA_HOME)/include" \
    -I"$(JAVA_HOME)/include/win32" \
    -shared -o [email protected] $<

libtestapp.so: TestApp.c
  $(CC) \
    -I"$(JAVA_HOME)/include" \
    -I"$(JAVA_HOME)/include/linux" \
    -fPIC \
    -o [email protected] -shared -Wl,-soname,testapp.so $<  \
    -z noexecstack

clean:
  rm -f *.o $(FINAL_TARGETS) ttyjni/*.class

TestApp.c

#include <jni.h>
#include <stdio.h>
#include "ttyjni_TestApp.h"

#if defined __CYGWIN__ || defined __MINGW32__ || defined __MINGW64__
#include <io.h>
#include <errno.h>
#include <wchar.h>
#include <windows.h>
#include <winternl.h>
#include <unistd.h>


/* vvvvvvvvvv From http://cygwin.com/ml/cygwin/2012-11/txt00003.txt vvvvvvvv */

#ifndef __MINGW64_VERSION_MAJOR
/* MS winternl.h defines FILE_INFORMATION_CLASS, but with only a
   different single member. */
enum FILE_INFORMATION_CLASSX
{
  FileNameInformation = 9
};

typedef struct _FILE_NAME_INFORMATION
{
  ULONG FileNameLength;
  WCHAR FileName[1];
} FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION;

NTSTATUS (NTAPI *pNtQueryInformationFile) (HANDLE, PIO_STATUS_BLOCK, PVOID,
    ULONG, FILE_INFORMATION_CLASSX);
#else
NTSTATUS (NTAPI *pNtQueryInformationFile) (HANDLE, PIO_STATUS_BLOCK, PVOID,
    ULONG, FILE_INFORMATION_CLASS);
#endif

jint
testapp_isatty(jint fd)
{
  HANDLE fh;
  NTSTATUS status;
  IO_STATUS_BLOCK io;
  long buf[66]; /* NAME_MAX + 1 + sizeof ULONG */
  PFILE_NAME_INFORMATION pfni = (PFILE_NAME_INFORMATION) buf;
  PWCHAR cp;


  /* First check using _isatty.

     Note that this returns the wrong result for NUL, for instance!
     Workaround is not to use _isatty at all, but rather GetFileType
     plus object name checking. */
  if (_isatty(fd))
    return 1;

  /* Now fetch the underlying HANDLE. */
  fh = (HANDLE)_get_osfhandle(fd);
  if (!fh || fh == INVALID_HANDLE_VALUE) {
    errno = EBADF;
    return 0;
  }

  /* Must be a pipe. */
  if (GetFileType (fh) != FILE_TYPE_PIPE)
    goto no_tty;

  /* Calling the native NT function NtQueryInformationFile is required to
     support pre-Vista systems.  If that of no concern, Vista introduced
     the GetFileInformationByHandleEx call with the FileNameInfo info class,
     which can be used instead. */
  if (!pNtQueryInformationFile) {
    pNtQueryInformationFile = (NTSTATUS (NTAPI *)(HANDLE, PIO_STATUS_BLOCK,
          PVOID, ULONG, FILE_INFORMATION_CLASS))
      GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQueryInformationFile");
    if (!pNtQueryInformationFile)
      goto no_tty;
  }
  if (!NT_SUCCESS (pNtQueryInformationFile (fh, &io, pfni, sizeof buf,
          FileNameInformation)))
    goto no_tty;

  /* The filename is not guaranteed to be NUL-terminated. */
  pfni->FileName[pfni->FileNameLength / sizeof (WCHAR)] = L'\0';

  /* Now check the name pattern.  The filename of a Cygwin pseudo tty pipe
     looks like this:

     \cygwin-%16llx-pty%d-{to,from}-master

     %16llx is the hash of the Cygwin installation, (to support multiple
     parallel installations), %d id the pseudo tty number, "to" or "from"
     differs the pipe direction. "from" is a stdin, "to" a stdout-like
     pipe. */
  cp = pfni->FileName;
  if (!wcsncmp(cp, L"\\cygwin-", 8)
      && !wcsncmp (cp + 24, L"-pty", 4))
  {
    cp = wcschr(cp + 28, '-');
    if (!cp)
      goto no_tty;
    if (!wcscmp (cp, L"-from-master") || !wcscmp (cp, L"-to-master"))
      return 1;
  }
no_tty:
  errno = EINVAL;
  return 0;
}

/* ^^^^^^^^^^ From http://cygwin.com/ml/cygwin/2012-11/txt00003.txt ^^^^^^^^ */

#elif _WIN32
#include <io.h>

static jint
testapp_isatty(jint fd)
{
  return _isatty(fd);
}
#elif defined __linux__ || defined __sun || defined __FreeBSD__
#include <unistd.h>

static jint
testapp_isatty(jint fd)
{
  return isatty(fd);
}
#else
#error Unsupported platform
#endif /* __CYGWIN__ */

JNIEXPORT jboolean JNICALL Java_ttyjni_TestApp_istty
(JNIEnv *env, jobject obj)
{
  return testapp_isatty(fileno(stdin)) &&
    testapp_isatty(fileno(stdout)) ?
    JNI_TRUE : JNI_FALSE;
}

ttyjni_TestApp.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class ttyjni_TestApp */

#ifndef _Included_ttyjni_TestApp
#define _Included_ttyjni_TestApp
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     ttyjni_TestApp
 * Method:    istty
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_ttyjni_TestApp_istty
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

ttyjni/TestApp.java

package ttyjni;

import java.io.Console;
import java.lang.reflect.Method;

class TestApp {
    static {
        System.loadLibrary("testapp");
    }
    private native boolean istty();

    private static final String ISTTY_METHOD = "istty";
    private static final String INTERACTIVE = "interactive";
    private static final String NON_INTERACTIVE = "non-interactive";

    protected static boolean isInteractive() {
        try {
            Method method = Console.class.getDeclaredMethod(ISTTY_METHOD);
            method.setAccessible(true);
            return (Boolean) method.invoke(Console.class);
        } catch (Exception e) {
            System.out.println(e.toString());
        }

        return false;
    }

    public static void main(String[] args) {
        // Testing JNI
        TestApp t = new TestApp();
        boolean b = t.istty();
        System.out.format("%s(jni)\n", b ?
                "interactive" : "non-interactive");

        // Testing pure Java
        System.out.format("%s(console)\n", System.console() != null ?
                INTERACTIVE : NON_INTERACTIVE);
        System.out.format("%s(java)\n", isInteractive() ?
                INTERACTIVE : NON_INTERACTIVE);
    }
}

test.sh

#!/bin/bash -
java -Djava.library.path="$(dirname "$0")" ttyjni.TestApp

Компиляция

make

Тестирование в Linux

$ ./test.sh
interactive(jni)
interactive(console)
interactive(java)

$ ./test.sh > 1
[email protected] ~/tmp/java $ cat 1
non-interactive(jni)
non-interactive(console)
non-interactive(java)

Тестирование на Cygwin

$ ./test.sh
interactive(jni)
non-interactive(console)
non-interactive(java)

$ ./test.sh > 1
$ cat 1
non-interactive(jni)
non-interactive(console)
non-interactive(java)