/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Command-line invocation of the Dalvik VM.
 */
#include "jni.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <assert.h>


/*
 * We want failed write() calls to just return with an error.
 */
static void blockSigpipe()
{
    sigset_t mask;

    sigemptyset(&mask);
    sigaddset(&mask, SIGPIPE);
    if (sigprocmask(SIG_BLOCK, &mask, NULL) != 0)
        fprintf(stderr, "WARNING: SIGPIPE not blocked\n");
}

/*
 * Create a String[] and populate it with the contents of argv.
 */
static jobjectArray createStringArray(JNIEnv* env, char* const argv[], int argc)
{
    jclass stringClass = NULL;
    jobjectArray strArray = NULL;
    jobjectArray result = NULL;
    int i;

    stringClass = (*env)->FindClass(env, "java/lang/String");
    if ((*env)->ExceptionCheck(env)) {
        fprintf(stderr, "Got exception while finding class String\n");
        goto bail;
    }
    assert(stringClass != NULL);
    strArray = (*env)->NewObjectArray(env, argc, stringClass, NULL);
    if ((*env)->ExceptionCheck(env)) {
        fprintf(stderr, "Got exception while creating String array\n");
        goto bail;
    }
    assert(strArray != NULL);

    for (i = 0; i < argc; i++) {
        jstring argStr;

        argStr = (*env)->NewStringUTF(env, argv[i]);
        if ((*env)->ExceptionCheck(env)) {
            fprintf(stderr, "Got exception while allocating Strings\n");
            goto bail;
        }
        assert(argStr != NULL);
        (*env)->SetObjectArrayElement(env, strArray, i, argStr);
        (*env)->DeleteLocalRef(env, argStr);
    }

    /* return the array, and ensure we don't delete the local ref to it */
    result = strArray;
    strArray = NULL;

bail:
    (*env)->DeleteLocalRef(env, stringClass);
    (*env)->DeleteLocalRef(env, strArray);
    return result;
}

/*
 * Determine whether or not the specified method is public.
 *
 * Returns JNI_TRUE on success, JNI_FALSE on failure.
 */
static int methodIsPublic(JNIEnv* env, jclass clazz, jmethodID methodId)
{
    static const int PUBLIC = 0x0001;   // java.lang.reflect.Modifiers.PUBLIC
    jobject refMethod = NULL;
    jclass methodClass = NULL;
    jmethodID getModifiersId;
    int modifiers;
    int result = JNI_FALSE;

    refMethod = (*env)->ToReflectedMethod(env, clazz, methodId, JNI_FALSE);
    if (refMethod == NULL) {
        fprintf(stderr, "Dalvik VM unable to get reflected method\n");
        goto bail;
    }

    /*
     * We now have a Method instance.  We need to call
     * its getModifiers() method.
     */
    methodClass = (*env)->FindClass(env, "java/lang/reflect/Method");
    if (methodClass == NULL) {
        fprintf(stderr, "Dalvik VM unable to find class Method\n");
        goto bail;
    }
    getModifiersId = (*env)->GetMethodID(env, methodClass,
                        "getModifiers", "()I");
    if (getModifiersId == NULL) {
        fprintf(stderr, "Dalvik VM unable to find reflect.Method.getModifiers\n");
        goto bail;
    }

    modifiers = (*env)->CallIntMethod(env, refMethod, getModifiersId);
    if ((modifiers & PUBLIC) == 0) {
        fprintf(stderr, "Dalvik VM: main() is not public\n");
        goto bail;
    }

    result = JNI_TRUE;

bail:
    (*env)->DeleteLocalRef(env, refMethod);
    (*env)->DeleteLocalRef(env, methodClass);
    return result;
}

/*
 * Parse arguments.  Most of it just gets passed through to the VM.  The
 * JNI spec defines a handful of standard arguments.
 */
int main(int argc, char* const argv[])
{
    JavaVM* vm = NULL;
    JNIEnv* env = NULL;
    JavaVMInitArgs initArgs;
    JavaVMOption* options = NULL;
    char* slashClass = NULL;
    int optionCount, curOpt, i, argIdx;
    int needExtra = JNI_FALSE;
    int result = 1;

    setvbuf(stdout, NULL, _IONBF, 0);

    /* ignore argv[0] */
    argv++;
    argc--;

    /*
     * If we're adding any additional stuff, e.g. function hook specifiers,
     * add them to the count here.
     *
     * We're over-allocating, because this includes the options to the VM
     * plus the options to the program.
     */
    optionCount = argc;

    options = (JavaVMOption*) malloc(sizeof(JavaVMOption) * optionCount);
    memset(options, 0, sizeof(JavaVMOption) * optionCount);

    /*
     * Copy options over.  Everything up to the name of the class starts
     * with a '-' (the function hook stuff is strictly internal).
     *
     * [Do we need to catch & handle "-jar" here?]
     */
    for (curOpt = argIdx = 0; argIdx < argc; argIdx++) {
        if (argv[argIdx][0] != '-' && !needExtra)
            break;
        options[curOpt++].optionString = strdup(argv[argIdx]);

        /* some options require an additional arg */
        needExtra = JNI_FALSE;
        if (strcmp(argv[argIdx], "-classpath") == 0 ||
            strcmp(argv[argIdx], "-cp") == 0)
            /* others? */
        {
            needExtra = JNI_TRUE;
        }
    }

    if (needExtra) {
        fprintf(stderr, "Dalvik VM requires value after last option flag\n");
        goto bail;
    }

    /* insert additional internal options here */

    assert(curOpt <= optionCount);

    initArgs.version = JNI_VERSION_1_4;
    initArgs.options = options;
    initArgs.nOptions = curOpt;
    initArgs.ignoreUnrecognized = JNI_FALSE;

    //printf("nOptions = %d\n", initArgs.nOptions);

    blockSigpipe();

    /*
     * Start VM.  The current thread becomes the main thread of the VM.
     */
    if (JNI_CreateJavaVM(&vm, &env, &initArgs) < 0) {
        fprintf(stderr, "Dalvik VM init failed (check log file)\n");
        goto bail;
    }

    /*
     * Make sure they provided a class name.  We do this after VM init
     * so that things like "-Xrunjdwp:help" have the opportunity to emit
     * a usage statement.
     */
    if (argIdx == argc) {
        fprintf(stderr, "Dalvik VM requires a class name\n");
        goto bail;
    }

    /*
     * We want to call main() with a String array with our arguments in it.
     * Create an array and populate it.  Note argv[0] is not included.
     */
    jobjectArray strArray;
    strArray = createStringArray(env, &argv[argIdx+1], argc-argIdx-1);
    if (strArray == NULL)
        goto bail;

    /*
     * Find [class].main(String[]).
     */
    jclass startClass;
    jmethodID startMeth;
    char* cp;

    /* convert "com.android.Blah" to "com/android/Blah" */
    slashClass = strdup(argv[argIdx]);
    for (cp = slashClass; *cp != '\0'; cp++)
        if (*cp == '.')
            *cp = '/';

    startClass = (*env)->FindClass(env, slashClass);
    if (startClass == NULL) {
        fprintf(stderr, "Dalvik VM unable to locate class '%s'\n", slashClass);
        goto bail;
    }

    startMeth = (*env)->GetStaticMethodID(env, startClass,
                    "main", "([Ljava/lang/String;)V");
    if (startMeth == NULL) {
        fprintf(stderr, "Dalvik VM unable to find static main(String[]) in '%s'\n",
            slashClass);
        goto bail;
    }

    /*
     * Make sure the method is public.  JNI doesn't prevent us from calling
     * a private method, so we have to check it explicitly.
     */
    if (!methodIsPublic(env, startClass, startMeth))
        goto bail;

    /*
     * Invoke main().
     */
    (*env)->CallStaticVoidMethod(env, startClass, startMeth, strArray);

    if (!(*env)->ExceptionCheck(env))
        result = 0;

bail:
    /*printf("Shutting down Dalvik VM\n");*/
    if (vm != NULL) {
        /*
         * This allows join() and isAlive() on the main thread to work
         * correctly, and also provides uncaught exception handling.
         */
        if ((*vm)->DetachCurrentThread(vm) != JNI_OK) {
            fprintf(stderr, "Warning: unable to detach main thread\n");
            result = 1;
        }

        if ((*vm)->DestroyJavaVM(vm) != 0)
            fprintf(stderr, "Warning: Dalvik VM did not shut down cleanly\n");
        /*printf("\nDalvik VM has exited\n");*/
    }

    for (i = 0; i < optionCount; i++)
        free((char*) options[i].optionString);
    free(options);
    free(slashClass);
    /*printf("--- VM is down, process exiting\n");*/
    return result;
}
