#include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_TAG "GitHugNativeGit" #define GIT_COMMAND_TIMEOUT_MS 30000 #define GIT_SESSION_MAX_READ_WAIT_MS 1000 typedef int (*git_main_fn)(int argc, const char **argv); typedef int (*githug_git_main_fn)(int argc, const char **argv); typedef void (*git_init_fn)(const char **argv); struct output_buffer { char *data; size_t length; size_t capacity; }; static pthread_mutex_t git_mutex = PTHREAD_MUTEX_INITIALIZER; static void *git_handle = NULL; static git_main_fn git_main = NULL; static githug_git_main_fn githug_git_main = NULL; static git_init_fn git_init = NULL; struct git_session { int id; pid_t child; int pty_fd; struct git_session *next; }; static struct git_session *git_sessions = NULL; static int next_git_session_id = 1; static int append_output(struct output_buffer *buffer, const char *data, size_t length) { if (length == 0) { return 0; } size_t required = buffer->length + length + 1; if (required > buffer->capacity) { size_t next_capacity = buffer->capacity == 0 ? 4096 : buffer->capacity; while (next_capacity < required) { next_capacity *= 2; } char *next = realloc(buffer->data, next_capacity); if (next == NULL) { return -1; } buffer->data = next; buffer->capacity = next_capacity; } memcpy(buffer->data + buffer->length, data, length); buffer->length += length; buffer->data[buffer->length] = '\0'; return 0; } static int drain_available_output(int fd, struct output_buffer *output, int *saw_eof) { char chunk[4096]; for (;;) { ssize_t count = read(fd, chunk, sizeof(chunk)); if (count > 0) { if (append_output(output, chunk, (size_t)count) != 0) { return -1; } continue; } if (count == 0) { *saw_eof = 1; return 0; } if (errno == EINTR) { continue; } if (errno == EAGAIN || errno == EWOULDBLOCK) { return 0; } return -1; } } static long long monotonic_millis(void) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) { return 0; } return ((long long)now.tv_sec * 1000LL) + ((long long)now.tv_nsec / 1000000LL); } static jobjectArray make_result(JNIEnv *env, int exit_code, const char *output) { jclass string_class = (*env)->FindClass(env, "java/lang/String"); jobjectArray result = (*env)->NewObjectArray(env, 2, string_class, NULL); char exit_text[32]; snprintf(exit_text, sizeof(exit_text), "%d", exit_code); jstring exit_string = (*env)->NewStringUTF(env, exit_text); jstring output_string = (*env)->NewStringUTF(env, output != NULL ? output : ""); (*env)->SetObjectArrayElement(env, result, 0, exit_string); (*env)->SetObjectArrayElement(env, result, 1, output_string); (*env)->DeleteLocalRef(env, exit_string); (*env)->DeleteLocalRef(env, output_string); return result; } static jobjectArray make_session_result(JNIEnv *env, int session_id, int running, int exit_code, const char *output) { jclass string_class = (*env)->FindClass(env, "java/lang/String"); jobjectArray result = (*env)->NewObjectArray(env, 4, string_class, NULL); char session_text[32]; char running_text[8]; char exit_text[32]; snprintf(session_text, sizeof(session_text), "%d", session_id); snprintf(running_text, sizeof(running_text), "%d", running ? 1 : 0); if (exit_code >= 0) { snprintf(exit_text, sizeof(exit_text), "%d", exit_code); } else { exit_text[0] = '\0'; } jstring session_string = (*env)->NewStringUTF(env, session_text); jstring running_string = (*env)->NewStringUTF(env, running_text); jstring exit_string = (*env)->NewStringUTF(env, exit_text); jstring output_string = (*env)->NewStringUTF(env, output != NULL ? output : ""); (*env)->SetObjectArrayElement(env, result, 0, session_string); (*env)->SetObjectArrayElement(env, result, 1, running_string); (*env)->SetObjectArrayElement(env, result, 2, exit_string); (*env)->SetObjectArrayElement(env, result, 3, output_string); (*env)->DeleteLocalRef(env, session_string); (*env)->DeleteLocalRef(env, running_string); (*env)->DeleteLocalRef(env, exit_string); (*env)->DeleteLocalRef(env, output_string); return result; } static jobjectArray make_error(JNIEnv *env, const char *message) { return make_result(env, -1, message); } static int load_git(const char *library_path) { if (git_init != NULL && git_main != NULL) { return 0; } git_handle = dlopen(library_path, RTLD_NOW | RTLD_GLOBAL); if (git_handle == NULL) { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "dlopen failed: %s", dlerror()); return -1; } git_init = (git_init_fn)dlsym(git_handle, "init_git"); if (git_init == NULL) { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "dlsym init_git failed: %s", dlerror()); return -1; } githug_git_main = (githug_git_main_fn)dlsym(git_handle, "githug_git_main"); git_main = (git_main_fn)dlsym(git_handle, "cmd_main"); if (git_main == NULL) { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "dlsym cmd_main failed: %s", dlerror()); return -1; } return 0; } static char **copy_string_array(JNIEnv *env, jobjectArray source, int *count_out) { int count = source == NULL ? 0 : (*env)->GetArrayLength(env, source); char **values = calloc((size_t)count + 1, sizeof(char *)); if (values == NULL) { return NULL; } for (int i = 0; i < count; i++) { jstring item = (jstring)(*env)->GetObjectArrayElement(env, source, i); const char *chars = (*env)->GetStringUTFChars(env, item, NULL); values[i] = strdup(chars != NULL ? chars : ""); (*env)->ReleaseStringUTFChars(env, item, chars); (*env)->DeleteLocalRef(env, item); if (values[i] == NULL) { for (int j = 0; j < i; j++) { free(values[j]); } free(values); return NULL; } } *count_out = count; return values; } static void free_string_array(char **values, int count) { if (values == NULL) { return; } for (int i = 0; i < count; i++) { free(values[i]); } free(values); } static struct git_session *find_session(int session_id, struct git_session ***link_out) { struct git_session **link = &git_sessions; while (*link != NULL) { if ((*link)->id == session_id) { if (link_out != NULL) { *link_out = link; } return *link; } link = &((*link)->next); } return NULL; } static void remove_session(struct git_session **link) { if (link == NULL) { return; } struct git_session *session = *link; if (session == NULL) { return; } *link = session->next; if (session->pty_fd >= 0) { close(session->pty_fd); } free(session); } static int child_exit_code_from_status(int status) { if (WIFEXITED(status)) { return WEXITSTATUS(status); } if (WIFSIGNALED(status)) { return 128 + WTERMSIG(status); } return -1; } struct saved_env { char *name; char *previous; int had_previous; }; static void restore_environment(struct saved_env *saved, int count) { for (int i = count - 1; i >= 0; i--) { if (saved[i].had_previous) { setenv(saved[i].name, saved[i].previous, 1); } else { unsetenv(saved[i].name); } free(saved[i].name); free(saved[i].previous); } free(saved); } static struct saved_env *apply_environment(char **entries, int count, int *applied_out) { struct saved_env *saved = calloc((size_t)count, sizeof(struct saved_env)); if (saved == NULL) { return NULL; } int applied = 0; for (int i = 0; i < count; i++) { char *separator = strchr(entries[i], '='); if (separator == NULL || separator == entries[i]) { continue; } *separator = '\0'; const char *name = entries[i]; const char *value = separator + 1; const char *previous = getenv(name); saved[applied].name = strdup(name); saved[applied].previous = previous == NULL ? NULL : strdup(previous); saved[applied].had_previous = previous != NULL; if (saved[applied].name == NULL || (previous != NULL && saved[applied].previous == NULL)) { *separator = '='; restore_environment(saved, applied + 1); return NULL; } setenv(name, value, 1); applied++; *separator = '='; } *applied_out = applied; return saved; } JNIEXPORT jobjectArray JNICALL Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative( JNIEnv *env, jclass clazz, jstring library_path, jstring working_directory, jobjectArray argv_array, jobjectArray environment_array ) { (void)clazz; const char *library_path_chars = (*env)->GetStringUTFChars(env, library_path, NULL); const char *working_directory_chars = (*env)->GetStringUTFChars(env, working_directory, NULL); int argc = 0; int envc = 0; char **argv = copy_string_array(env, argv_array, &argc); char **env_entries = copy_string_array(env, environment_array, &envc); if (library_path_chars == NULL || working_directory_chars == NULL || argv == NULL || env_entries == NULL) { return make_error(env, "Native Git execution failed: out of memory"); } int pipe_fds[2]; if (pipe(pipe_fds) != 0) { free_string_array(argv, argc); free_string_array(env_entries, envc); (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); return make_error(env, "Native Git execution failed: could not create output pipe"); } pthread_mutex_lock(&git_mutex); if (load_git(library_path_chars) != 0) { const char *error = dlerror(); close(pipe_fds[0]); close(pipe_fds[1]); pthread_mutex_unlock(&git_mutex); free_string_array(argv, argc); free_string_array(env_entries, envc); (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); return make_error(env, error != NULL ? error : "Native Git execution failed: init_git/cmd_main not found"); } pid_t child = fork(); if (child < 0) { close(pipe_fds[0]); close(pipe_fds[1]); pthread_mutex_unlock(&git_mutex); free_string_array(argv, argc); free_string_array(env_entries, envc); (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); return make_error(env, "Native Git execution failed: could not fork"); } if (child == 0) { setpgid(0, 0); close(pipe_fds[0]); dup2(pipe_fds[1], STDOUT_FILENO); dup2(pipe_fds[1], STDERR_FILENO); close(pipe_fds[1]); int applied_env = 0; struct saved_env *saved_env = apply_environment(env_entries, envc, &applied_env); (void)saved_env; chdir(working_directory_chars); int child_exit_code; if (githug_git_main != NULL) { child_exit_code = githug_git_main(argc, (const char **)argv); } else { git_init((const char **)argv); child_exit_code = git_main(argc, (const char **)argv); } fflush(stdout); fflush(stderr); _exit(child_exit_code); } setpgid(child, child); close(pipe_fds[1]); struct output_buffer output = {0}; int flags = fcntl(pipe_fds[0], F_GETFL, 0); if (flags >= 0) { fcntl(pipe_fds[0], F_SETFL, flags | O_NONBLOCK); } int child_status = 0; int child_done = 0; int saw_eof = 0; int timed_out = 0; long long started_at = monotonic_millis(); while (!child_done || !saw_eof) { if (drain_available_output(pipe_fds[0], &output, &saw_eof) != 0) { break; } if (!child_done) { pid_t wait_result = waitpid(child, &child_status, WNOHANG); if (wait_result == child) { child_done = 1; } else if (wait_result < 0 && errno != EINTR) { child_done = 1; } } if (!child_done && started_at > 0 && monotonic_millis() - started_at > GIT_COMMAND_TIMEOUT_MS) { timed_out = 1; kill(-child, SIGKILL); kill(child, SIGKILL); while (waitpid(child, &child_status, 0) < 0 && errno == EINTR) { } child_done = 1; const char *timeout_message = "\nNative Git execution timed out.\n"; append_output(&output, timeout_message, strlen(timeout_message)); } if (child_done) { if (!saw_eof) { drain_available_output(pipe_fds[0], &output, &saw_eof); } break; } usleep(10000); } close(pipe_fds[0]); pthread_mutex_unlock(&git_mutex); int exit_code = -1; if (timed_out) { exit_code = 124; } else if (WIFEXITED(child_status)) { exit_code = WEXITSTATUS(child_status); } else if (WIFSIGNALED(child_status)) { exit_code = 128 + WTERMSIG(child_status); } jobjectArray result = make_result(env, exit_code, output.data); free(output.data); free_string_array(argv, argc); free_string_array(env_entries, envc); (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); return result; } JNIEXPORT jobjectArray JNICALL Java_solutions_tretter_githugandroid_NativeGitBridge_startGitSessionNative( JNIEnv *env, jclass clazz, jstring library_path, jstring working_directory, jobjectArray argv_array, jobjectArray environment_array ) { (void)clazz; const char *library_path_chars = (*env)->GetStringUTFChars(env, library_path, NULL); const char *working_directory_chars = (*env)->GetStringUTFChars(env, working_directory, NULL); int argc = 0; int envc = 0; char **argv = copy_string_array(env, argv_array, &argc); char **env_entries = copy_string_array(env, environment_array, &envc); if (library_path_chars == NULL || working_directory_chars == NULL || argv == NULL || env_entries == NULL) { return make_session_result(env, 0, 0, -1, "Native Git session failed: out of memory"); } int master_fd = posix_openpt(O_RDWR | O_NOCTTY); if (master_fd < 0 || grantpt(master_fd) != 0 || unlockpt(master_fd) != 0) { if (master_fd >= 0) { close(master_fd); } free_string_array(argv, argc); free_string_array(env_entries, envc); (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); return make_session_result(env, 0, 0, -1, "Native Git session failed: could not create PTY"); } char *slave_name = ptsname(master_fd); if (slave_name == NULL) { close(master_fd); free_string_array(argv, argc); free_string_array(env_entries, envc); (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); return make_session_result(env, 0, 0, -1, "Native Git session failed: could not resolve PTY slave"); } pthread_mutex_lock(&git_mutex); if (load_git(library_path_chars) != 0) { close(master_fd); pthread_mutex_unlock(&git_mutex); free_string_array(argv, argc); free_string_array(env_entries, envc); (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); return make_session_result(env, 0, 0, -1, "Native Git session failed: init_git/cmd_main not found"); } pid_t child = fork(); if (child < 0) { close(master_fd); pthread_mutex_unlock(&git_mutex); free_string_array(argv, argc); free_string_array(env_entries, envc); (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); return make_session_result(env, 0, 0, -1, "Native Git session failed: could not fork"); } if (child == 0) { setsid(); int slave_fd = open(slave_name, O_RDWR); if (slave_fd < 0) { _exit(127); } ioctl(slave_fd, TIOCSCTTY, 0); dup2(slave_fd, STDIN_FILENO); dup2(slave_fd, STDOUT_FILENO); dup2(slave_fd, STDERR_FILENO); if (slave_fd > STDERR_FILENO) { close(slave_fd); } close(master_fd); int applied_env = 0; struct saved_env *saved_env = apply_environment(env_entries, envc, &applied_env); (void)saved_env; chdir(working_directory_chars); int child_exit_code; if (githug_git_main != NULL) { child_exit_code = githug_git_main(argc, (const char **)argv); } else { git_init((const char **)argv); child_exit_code = git_main(argc, (const char **)argv); } fflush(stdout); fflush(stderr); _exit(child_exit_code); } setpgid(child, child); int flags = fcntl(master_fd, F_GETFL, 0); if (flags >= 0) { fcntl(master_fd, F_SETFL, flags | O_NONBLOCK); } struct git_session *session = calloc(1, sizeof(struct git_session)); if (session == NULL) { kill(-child, SIGKILL); close(master_fd); pthread_mutex_unlock(&git_mutex); free_string_array(argv, argc); free_string_array(env_entries, envc); (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); return make_session_result(env, 0, 0, -1, "Native Git session failed: out of memory"); } session->id = next_git_session_id++; session->child = child; session->pty_fd = master_fd; session->next = git_sessions; git_sessions = session; struct output_buffer output = {0}; int saw_eof = 0; long long started_at = monotonic_millis(); while (started_at > 0 && monotonic_millis() - started_at < GIT_SESSION_MAX_READ_WAIT_MS) { drain_available_output(session->pty_fd, &output, &saw_eof); if (output.length > 0) { break; } usleep(10000); } int session_id = session->id; int status = 0; int running = 1; int exit_code = -1; pid_t wait_result = waitpid(child, &status, WNOHANG); if (wait_result == child) { running = 0; exit_code = child_exit_code_from_status(status); struct git_session **link = NULL; find_session(session_id, &link); remove_session(link); } jobjectArray result = make_session_result(env, session_id, running, exit_code, output.data); free(output.data); pthread_mutex_unlock(&git_mutex); free_string_array(argv, argc); free_string_array(env_entries, envc); (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); return result; } JNIEXPORT jobjectArray JNICALL Java_solutions_tretter_githugandroid_NativeGitBridge_writeGitSessionNative( JNIEnv *env, jclass clazz, jint session_id, jstring input ) { (void)clazz; const char *input_chars = (*env)->GetStringUTFChars(env, input, NULL); pthread_mutex_lock(&git_mutex); struct git_session **link = NULL; struct git_session *session = find_session((int)session_id, &link); if (session == NULL) { pthread_mutex_unlock(&git_mutex); if (input_chars != NULL) { (*env)->ReleaseStringUTFChars(env, input, input_chars); } return make_session_result(env, (int)session_id, 0, -1, "Native Git session is not running."); } if (input_chars != NULL) { size_t input_length = strlen(input_chars); size_t written = 0; while (written < input_length) { ssize_t count = write(session->pty_fd, input_chars + written, input_length - written); if (count > 0) { written += (size_t)count; } else if (errno != EINTR) { break; } } (*env)->ReleaseStringUTFChars(env, input, input_chars); } struct output_buffer output = {0}; int saw_eof = 0; long long started_at = monotonic_millis(); while (started_at > 0 && monotonic_millis() - started_at < GIT_SESSION_MAX_READ_WAIT_MS) { drain_available_output(session->pty_fd, &output, &saw_eof); if (output.length > 0 || saw_eof) { break; } usleep(10000); } int status = 0; int running = 1; int exit_code = -1; pid_t wait_result = waitpid(session->child, &status, WNOHANG); if (wait_result == session->child || saw_eof) { if (wait_result != session->child) { while (waitpid(session->child, &status, 0) < 0 && errno == EINTR) { } } running = 0; exit_code = child_exit_code_from_status(status); remove_session(link); } jobjectArray result = make_session_result(env, (int)session_id, running, exit_code, output.data); free(output.data); pthread_mutex_unlock(&git_mutex); return result; }