Fix the issue causing the Godot Android Editor to crash when returning from the launched and running game

The issue was caused because the running game pid was not set, and thus had a value of `0`. When trying to stop the running game, the `EditorRun::stop()` logic would kill the process with pid 0, which on Android corresponds to the running app's own process, thus causing the editor to crash.
This issue did not happen on Godot 3 because pid with value of `0` are not considered valid.
This commit is contained in:
Fredia Huya-Kouadio 2023-01-26 02:55:47 -08:00
parent 18a2e7ff6e
commit ec4d720850
9 changed files with 132 additions and 48 deletions

View file

@ -272,7 +272,9 @@ Error EditorRun::run(const String &p_scene, const String &p_write_movie) {
OS::ProcessID pid = 0;
Error err = OS::get_singleton()->create_instance(args, &pid);
ERR_FAIL_COND_V(err, err);
pids.push_back(pid);
if (pid != 0) {
pids.push_back(pid);
}
}
status = STATUS_PLAY;

View file

@ -31,12 +31,11 @@
package org.godotengine.editor
import android.Manifest
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Debug
import android.os.Environment
import android.os.*
import android.util.Log
import android.widget.Toast
import androidx.window.layout.WindowMetricsCalculator
@ -64,11 +63,18 @@ open class GodotEditor : FullScreenGodotApp() {
private const val COMMAND_LINE_PARAMS = "command_line_params"
private const val EDITOR_ID = 777
private const val EDITOR_ARG = "--editor"
private const val EDITOR_ARG_SHORT = "-e"
private const val EDITOR_PROCESS_NAME_SUFFIX = ":GodotEditor"
private const val GAME_ID = 667
private const val GAME_PROCESS_NAME_SUFFIX = ":GodotGame"
private const val PROJECT_MANAGER_ID = 555
private const val PROJECT_MANAGER_ARG = "--project-manager"
private const val PROJECT_MANAGER_ARG_SHORT = "-p"
private const val PROJECT_MANAGER_PROCESS_NAME_SUFFIX = ":GodotProjectManager"
}
private val commandLineParams = ArrayList<String>()
@ -102,9 +108,10 @@ open class GodotEditor : FullScreenGodotApp() {
override fun getCommandLine() = commandLineParams
override fun onNewGodotInstanceRequested(args: Array<String>) {
override fun onNewGodotInstanceRequested(args: Array<String>): Int {
// Parse the arguments to figure out which activity to start.
var targetClass: Class<*> = GodotGame::class.java
var instanceId = GAME_ID
// Whether we should launch the new godot instance in an adjacent window
// https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT
@ -115,12 +122,14 @@ open class GodotEditor : FullScreenGodotApp() {
if (EDITOR_ARG == arg || EDITOR_ARG_SHORT == arg) {
targetClass = GodotEditor::class.java
launchAdjacent = false
instanceId = EDITOR_ID
break
}
if (PROJECT_MANAGER_ARG == arg || PROJECT_MANAGER_ARG_SHORT == arg) {
targetClass = GodotProjectManager::class.java
launchAdjacent = false
instanceId = PROJECT_MANAGER_ID
break
}
}
@ -139,6 +148,37 @@ open class GodotEditor : FullScreenGodotApp() {
Log.d(TAG, "Starting $targetClass")
startActivity(newInstance)
}
return instanceId
}
override fun onGodotForceQuit(godotInstanceId: Int): Boolean {
val processNameSuffix = when (godotInstanceId) {
GAME_ID -> {
GAME_PROCESS_NAME_SUFFIX
}
EDITOR_ID -> {
EDITOR_PROCESS_NAME_SUFFIX
}
PROJECT_MANAGER_ID -> {
PROJECT_MANAGER_PROCESS_NAME_SUFFIX
}
else -> ""
}
if (processNameSuffix.isBlank()) {
return false
}
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val runningProcesses = activityManager.runningAppProcesses
for (runningProcess in runningProcesses) {
if (runningProcess.processName.endsWith(processNameSuffix)) {
Log.v(TAG, "Killing Godot process ${runningProcess.processName}")
Process.killProcess(runningProcess.pid)
return true
}
}
return false
}
// Get the screen's density scale

View file

@ -74,28 +74,36 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God
public void onDestroy() {
Log.v(TAG, "Destroying Godot app...");
super.onDestroy();
onGodotForceQuit(godotFragment);
terminateGodotInstance(godotFragment);
}
@Override
public final void onGodotForceQuit(Godot instance) {
runOnUiThread(() -> {
terminateGodotInstance(instance);
});
}
private void terminateGodotInstance(Godot instance) {
if (instance == godotFragment) {
Log.v(TAG, "Force quitting Godot instance");
ProcessPhoenix.forceQuit(this);
ProcessPhoenix.forceQuit(FullScreenGodotApp.this);
}
}
@Override
public final void onGodotRestartRequested(Godot instance) {
if (instance == godotFragment) {
// It's very hard to properly de-initialize Godot on Android to restart the game
// from scratch. Therefore, we need to kill the whole app process and relaunch it.
//
// Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
// releasing and reloading native libs or resetting their state somehow and clearing statics).
Log.v(TAG, "Restarting Godot instance...");
ProcessPhoenix.triggerRebirth(this);
}
runOnUiThread(() -> {
if (instance == godotFragment) {
// It's very hard to properly de-initialize Godot on Android to restart the game
// from scratch. Therefore, we need to kill the whole app process and relaunch it.
//
// Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
// releasing and reloading native libs or resetting their state somehow and clearing statics).
Log.v(TAG, "Restarting Godot instance...");
ProcessPhoenix.triggerRebirth(FullScreenGodotApp.this);
}
});
}
@Override

View file

@ -348,11 +348,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
}
public void restart() {
runOnUiThread(() -> {
if (godotHost != null) {
godotHost.onGodotRestartRequested(this);
}
});
if (godotHost != null) {
godotHost.onGodotRestartRequested(this);
}
}
public void alert(final String message, final String title) {
@ -889,11 +887,20 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
private void forceQuit() {
// TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each
// native Godot components that is started in Godot#onVideoInit.
runOnUiThread(() -> {
if (godotHost != null) {
godotHost.onGodotForceQuit(this);
}
});
forceQuit(0);
}
@Keep
private boolean forceQuit(int instanceId) {
if (godotHost == null) {
return false;
}
if (instanceId == 0) {
godotHost.onGodotForceQuit(this);
return true;
} else {
return godotHost.onGodotForceQuit(instanceId);
}
}
private boolean obbIsCorrupted(String f, String main_pack_md5) {
@ -1052,11 +1059,10 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
}
@Keep
private void createNewGodotInstance(String[] args) {
runOnUiThread(() -> {
if (godotHost != null) {
godotHost.onNewGodotInstanceRequested(args);
}
});
private int createNewGodotInstance(String[] args) {
if (godotHost != null) {
return godotHost.onNewGodotInstanceRequested(args);
}
return 0;
}
}

View file

@ -55,21 +55,35 @@ public interface GodotHost {
default void onGodotMainLoopStarted() {}
/**
* Invoked on the UI thread as the last step of the Godot instance clean up phase.
* Invoked on the render thread to terminate the given Godot instance.
*/
default void onGodotForceQuit(Godot instance) {}
/**
* Invoked on the UI thread when the Godot instance wants to be restarted. It's up to the host
* Invoked on the render thread to terminate the Godot instance with the given id.
* @param godotInstanceId id of the Godot instance to terminate. See {@code onNewGodotInstanceRequested}
*
* @return true if successful, false otherwise.
*/
default boolean onGodotForceQuit(int godotInstanceId) {
return false;
}
/**
* Invoked on the render thread when the Godot instance wants to be restarted. It's up to the host
* to perform the appropriate action(s).
*/
default void onGodotRestartRequested(Godot instance) {}
/**
* Invoked on the UI thread when a new Godot instance is requested. It's up to the host to
* Invoked on the render thread when a new Godot instance is requested. It's up to the host to
* perform the appropriate action(s).
*
* @param args Arguments used to initialize the new instance.
*
* @return the id of the new instance. See {@code onGodotForceQuit}
*/
default void onNewGodotInstanceRequested(String[] args) {}
default int onNewGodotInstanceRequested(String[] args) {
return 0;
}
}

View file

@ -60,7 +60,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
// get some Godot method pointers...
_on_video_init = p_env->GetMethodID(godot_class, "onVideoInit", "()Z");
_restart = p_env->GetMethodID(godot_class, "restart", "()V");
_finish = p_env->GetMethodID(godot_class, "forceQuit", "()V");
_finish = p_env->GetMethodID(godot_class, "forceQuit", "(I)Z");
_set_keep_screen_on = p_env->GetMethodID(godot_class, "setKeepScreenOn", "(Z)V");
_alert = p_env->GetMethodID(godot_class, "alert", "(Ljava/lang/String;Ljava/lang/String;)V");
_get_GLES_version_code = p_env->GetMethodID(godot_class, "getGLESVersionCode", "()I");
@ -77,7 +77,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;");
_on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V");
_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
_create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)V");
_create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I");
_get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;");
// get some Activity method pointers...
@ -179,14 +179,15 @@ void GodotJavaWrapper::restart(JNIEnv *p_env) {
}
}
void GodotJavaWrapper::force_quit(JNIEnv *p_env) {
bool GodotJavaWrapper::force_quit(JNIEnv *p_env, int p_instance_id) {
if (_finish) {
if (p_env == nullptr) {
p_env = get_jni_env();
}
ERR_FAIL_NULL(p_env);
p_env->CallVoidMethod(godot_instance, _finish);
ERR_FAIL_NULL_V(p_env, false);
return p_env->CallBooleanMethod(godot_instance, _finish, p_instance_id);
}
return false;
}
void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) {
@ -345,14 +346,16 @@ void GodotJavaWrapper::vibrate(int p_duration_ms) {
}
}
void GodotJavaWrapper::create_new_godot_instance(List<String> args) {
int GodotJavaWrapper::create_new_godot_instance(List<String> args) {
if (_create_new_godot_instance) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL(env);
ERR_FAIL_NULL_V(env, 0);
jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF(""));
for (int i = 0; i < args.size(); i++) {
env->SetObjectArrayElement(jargs, i, env->NewStringUTF(args[i].utf8().get_data()));
}
env->CallVoidMethod(godot_instance, _create_new_godot_instance, jargs);
return env->CallIntMethod(godot_instance, _create_new_godot_instance, jargs);
} else {
return 0;
}
}

View file

@ -85,7 +85,7 @@ public:
void on_godot_setup_completed(JNIEnv *p_env = nullptr);
void on_godot_main_loop_started(JNIEnv *p_env = nullptr);
void restart(JNIEnv *p_env = nullptr);
void force_quit(JNIEnv *p_env = nullptr);
bool force_quit(JNIEnv *p_env = nullptr, int p_instance_id = 0);
void set_keep_screen_on(bool p_enabled);
void alert(const String &p_message, const String &p_title);
int get_gles_version_code();
@ -103,7 +103,7 @@ public:
bool is_activity_resumed();
void vibrate(int p_duration_ms);
String get_input_fallback_mapping();
void create_new_godot_instance(List<String> args);
int create_new_godot_instance(List<String> args);
};
#endif // JAVA_GODOT_WRAPPER_H

View file

@ -739,9 +739,19 @@ Error OS_Android::create_process(const String &p_path, const List<String> &p_arg
}
Error OS_Android::create_instance(const List<String> &p_arguments, ProcessID *r_child_id) {
godot_java->create_new_godot_instance(p_arguments);
int instance_id = godot_java->create_new_godot_instance(p_arguments);
if (r_child_id) {
*r_child_id = instance_id;
}
return OK;
}
Error OS_Android::kill(const ProcessID &p_pid) {
if (godot_java->force_quit(nullptr, p_pid)) {
return OK;
}
return OS_Unix::kill(p_pid);
}
OS_Android::~OS_Android() {
}

View file

@ -158,6 +158,7 @@ public:
virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override;
virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override;
virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override;
virtual Error kill(const ProcessID &p_pid) override;
virtual bool _check_internal_feature_support(const String &p_feature) override;
OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion);