birdofpreyru / react-native-static-server

Embedded HTTP server for React Native
https://dr.pogodin.studio/docs/react-native-static-server
Other
127 stars 20 forks source link

Server is Crashing on Android - React Native Expo App #26

Open jyotipixolo opened 1 year ago

jyotipixolo commented 1 year ago

The plugin is working fine on IOS. But the server is getting crashed on Android after some time.

Package JSON

{
  "name": "otg-olp-mobile-app",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start --dev-client",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "@dr.pogodin/react-native-static-server": "^0.7.2",
    "@react-native-async-storage/async-storage": "~1.17.3",
    "@react-native-community/netinfo": "^9.3.5",
    "@react-navigation/native": "^6.1.2",
    "@react-navigation/stack": "^6.3.11",
    "axios": "^1.3.3",
    "expo": "~47.0.12",
    "expo-file-system": "^15.2.2",
    "expo-font": "~11.0.1",
    "expo-progress": "^0.0.2",
    "expo-screen-orientation": "~5.0.1",
    "expo-splash-screen": "~0.17.5",
    "expo-sqlite": "^11.0.0",
    "expo-status-bar": "~1.4.2",
    "react": "18.1.0",
    "react-dom": "18.1.0",
    "react-native": "0.70.5",
    "react-native-fs": "^2.20.0",
    "react-native-gesture-handler": "~2.8.0",
    "react-native-keyboard-aware-scroll-view": "^0.9.5",
    "react-native-paper": "^5.2.0",
    "react-native-safe-area-context": "4.4.1",
    "react-native-screens": "~3.18.0",
    "react-native-svg": "13.4.0",
    "react-native-svg-transformer": "^1.0.0",
    "react-native-web": "~0.18.9",
    "react-native-webview": "11.23.1",
    "react-native-xml2js": "^1.0.3",
    "react-native-zip-archive": "^6.0.9"
  },
  "devDependencies": {
    "@babel/core": "^7.12.9",
    "@types/react": "^18.0.28",
    "@types/react-native": "^0.71.3",
    "typescript": "^4.6.3"
  },
  "private": true
}

The way my app works is. I download a zip folder from a remote URL, I then extract it to a folder in RNFS /scorm-player/  Similarly, I download multiple modules to the same directory. Each module being anywhere between 20 MB - 160 MB in size. All of this happens before I start the local server. When I open the page where my Local Server is needed. I'm using this code to run the server. 


// Function to check if the server is active or not and create a new one if not
    const checkAndCreateServer = () => {
        // Get the active server
        let server = getActiveServer();
        if (!server) {
            // If no server is active, create a new one
            server = new Server({
                fileDir: RNFS.DocumentDirectoryPath + "/scorm-player",
                stopInBackground: true,
            });
        }
        // If the server is inactive, start it
        if (server) {
            if (server.state === STATES.INACTIVE) {
                server.start().then((url) => {
                    console.log("Server started at: ", url);
                    setLocalHostURI(url);
                });
            } else {
                setLocalHostURI(server.origin);
            }
        }
    };

Before I can update the SRC in the webview, I need to get some data (from a local SQLite database) about the module I want to play in the Webview, and create GET REQUEST PARAM string to pass in the Source URL. This works perfectly fine when I'm testing it on a iOS Simulator. But when I run the same thing on an Android Simulator. It works perfectly the first time. After that when I go back and open the page again, the app freezes completely, without throwing any exception. And then I see a console in Android Studio which says "Server crashed"

WhatsApp Image 2023-03-18 at 19 05 22

birdofpreyru commented 1 year ago

Hi @jyotipixolo , I think you might be doing it wrong: with stopInBackground: true the server will automatically stop when the app goes into background (and it will make its state INACTIVE), and then it will automatically re-start when the app is in the foreground again. The check if (server.state === STATES.INACTIVE) { in such scenario still may evaluate true, and thus trigger .start() once again, causing the crash.

As a rapid test, can you try adding server.stop() call just before server.start()? For server in INACTIVE state it will ensure no automatic re-start will happen until the .start() is explicitly called. If that does not solve the issue, then there is some bug to fix in the library, otherwise I'd say your code around the server should be refactored.

jyotipixolo commented 1 year ago

Hi @birdofpreyru, I tried stopping the server before starting it. To make things easier I took out all of my other code regarding the data and now I am just starting(After stopping) the server when I land on the screen and stopping when I exit.


        // Function to check if the server is active or not and create a new one if not
    const checkAndCreateServer = async () => {
        // Get the active server - get the running server
        let server = getActiveServer();
        if (!server) {
            // If no server is active, create a new one
            server = new Server({
                fileDir: RNFS.DocumentDirectoryPath + "/scorm-player",
                nonLocal: true,
            });
        }
        if (server) {
            await server.stop();
            server.start().then((url) => {
                console.log("Server started at: ", url);
                setLocalHostURI(url);
            });
        }
    };
           useEffect(() => {
            checkAndCreateServer();
        }, []);
        const backAction = async () => {
        // Stop the server
        const server = getActiveServer();
        if (server && server.state === STATES.ACTIVE) {
              await  server.stop();
        }
        navigation.goBack();
    };

But my server still crashes. And the crash happens at random moments, sometimes on server start and sometimes on server stop. Most of the times the server starts successfully the first time but causes issues when I go back from the screen and return to it.

Could this be a memory issue in android ? Is there a better way to handle the stopping of the server?

I don't always get the Server Crashed exception in logcat but the app still freezes. Although, when comment the server code my app runs smoothly.

birdofpreyru commented 1 year ago

Agh... you know, getActiveServer() only returns a server, if any, when it is non-INACTIVE, and non-CRASHED. Doing what you doing, I guess, you are leaking some created server instances because they were INACTIVE when you call getActiveServer(), and attempting to create and start new server instances you eventually crash the app.

You should do at least as in the example app, where the hook just keeps server reference and stops it on unmount, i.e. (over-simplifying the example):

useEffect(() => {
  const server = new Server(..);
  ...
  server.start(..).then(..);
  return () => {
    server.stop();
  };
}, []);

this way you don't leak references to the server instances you create, and ensure that every server is correctly shutdown when the hook unmounts. Note, even with such pattern, the example does it in the root app component, thus it is sure the hook never executed more than once a time in the app (in contrast to if you put it into some component, which can be mounted and unmounted). If you do it in some component, probably getActiveServer() is not enough to synchronize operation of multiple server instances... although I added it into lib, I haven't gave it too much thought, as in my projects I always rely on a single server instance in the root of the app, like in the example.

ZakiPathan2010 commented 1 year ago

I also got the same issue. I have expo bare workflow project. What I already try till now: 1) Stop the server when screen closed 2) Wait until server is stop with then & catch block. In then block I do navigation.goBack() 3) Never stop the server 4) Check if any active server is there if not then only create & start a new server 5) Stop previous server before starting a new one

Nothing works. App is freezing randomly on different points. & without server code everything is working fine.

I am attaching one simple project with server code with 2 screens in it, just starting & stopping the server in different ways. You can check it here : https://we.tl/t-dZ1nq3dmQk

birdofpreyru commented 1 year ago

@ZakiPathan2010 please, re-read my previous comment. I see in your giant archive the same code fragment I commented about above, which potentially creates multiple server instances when the screen mounts/unmounts, because there is no syncrhonization between different screen instances, I guess; also it does not stop the server when screen is unmounted — I don't know what are you trying to do, but probably you'll do better if you just create a single server instance at the root of the app, and stop it when you are sure you don't need it.

jyotipixolo commented 1 year ago

One more thing that needs to be considered is that my code is working absolutely fine in the IOS simulator and on a real device. But the server is crashing on Android.

birdofpreyru commented 1 year ago

Well, on Android I use it in several app distributed via Google.Play, and installed on ~5k devices in total. All set up to report me errors and crashes via Sentry. So far I haven't seen the servers crashing there. Maybe I miss something; maybe your code works fine in ios and simulator because they implement the execution flow sync between js and native differently and it prevents potential issues with your code i pointed above.

I'll be on the lookout for crashes on Android, but so far haven't seen them myself, but I also use it the way demoed in the example app.

jyotipixolo commented 1 year ago

I tried clearing all the server instances before I start a new server. I made a function that returned an array of all the servers (in any state they are in). This always returned an empty array, only then I started a new server. But the freezing still exists. The server crash happens inside the "launch" function in the server.java file. Because any log after that is not seen. I also put the start server code inside a useEffect in my app.tsx too, with a plan that I start the server initially and keep it that way till I need it in my inner page. But I don't even get past my Login Screen and the server crashes and the app freezes. Is these a version of the plugin that is more suited for Android?

birdofpreyru commented 1 year ago

The server crash happens inside the "launch" function in the server.java file.

That launch() function is the JNI entry-point into the underlying C-code of Lighttpd server.

any log after that is not seen

You actually can see logs from Lighttpd in adb logcat outputs; and if you uncomment these lines in the Lighttpd config the library generates internally, you even can direct all log output from Lighttpd core into a dedicated file, that later can be found and inspected with adb.

I don't even get past my Login Screen and the server crashes and the app freezes

I believe, earlier you told "Most of the times the server starts successfully the first time but causes issues when I go back from the screen and return to it", which to me indicates that you do some mistakes in handling those restarts.

Is these a version of the plugin that is more suited for Android?

No, there is no special version for Android, I rely on the same latest versions I released on NPM.

jyotipixolo commented 1 year ago

I'm just trying to make it work now. So this is what I'm doing in my App.tsx. I tried to replicate the example code. I have taken out any restart code, or any references to the plugin on any other components.

The server starts normally like always.

My App.tsx has a stack of screens, the initial one being the Login Screen. While I'm still on the Login Screen the server crashes.

useEffect(() => {
  let server: null | Server = new Server({
    fileDir: RNFS.DocumentDirectoryPath + "/scorm-player",
    stopInBackground: true,
  });
  (async () => {
    const res = await server?.start();
    if (res && server) {
      console.log("Server is running");
    }
  })();
  return () => {
    (async () => {
      server?.stop();
      server = null;
    })();
  };
}, []);

These are the Logs I feel would be relevant.

**lighttpd** - E - (../../../../../lighttpd1.4/src/server.c.1057) [note] graceful shutdown started **lighttpd** - E - (../../../../../lighttpd1.4/src/server.c.2082) server stopped by UID = 10716 PID = 6606 **RN_STATIC_SERVER** - I - Res -1 **RN_STATIC_SERVER** - E - Server crashed java.lang.Exception: Native server exited with status -1 at com.lighttpd.Server.run(Server.java:82)

The above logs were formed without server.stop() being called.

I noticed another log, way before the crash happened. **ReactNativeJNI** - I - Memory warning (pressure level: TRIM_MEMORY_RUNNING_CRITICAL) received by JS VM, running a GC But again, when I'm not starting the server, the freezing issue stops.

Do you think there are any other packages in my code that might be clashing with yours?

birdofpreyru commented 1 year ago

Well... are you sure there is nothing else in your app consuming too much memory, and thus triggering OS to garbage-collect / kill stuff?

ReactNativeJNI - I - Memory warning (pressure level: TRIM_MEMORY_RUNNING_CRITICAL) received by JS VM, running a GC

:point_up: This reads like, yeah, too much memory consumed, OS started to fight for resources.

lighttpd - E - (../../../../../lighttpd1.4/src/server.c.1057) [note] graceful shutdown started lighttpd - E - (../../../../../lighttpd1.4/src/server.c.2082) server stopped by UID = 10716 PID = 6606

:point_up: These actually come from Lighttpd core. Should be looked up there, but the way they read... looks like the server attempted and completed a graceful shutdown, the same like when .stop() is called. If you haven't asked for it, neither app transition to background, alongside with stopInBackground flag caused it... I believe Lighttpd listen for OS interrupts, I guess if Android runs out of memory, and sends some interrupts to app processes asking them to quit, if possible, to reclaim some memory, probably that can trigger server's graceful shutdown and exit with non-zero status code — this all should be checked, I am just saying out of my head, not really knowing this stuff in details.

RN_STATIC_SERVER - I - Res -1 RN_STATIC_SERVER - E - Server crashed java.lang.Exception: Native server exited with status -1 at com.lighttpd.Server.run(Server.java:82)

:point_up: And this comes from Java layer of this library, specifically from this point. As you see, there is a trivial logic behind it — if process exited with non-zero status — throw exception, say it crashed.

So, if you wanna look into this further yourself, I guess the next thing is to look into Lighttpd sources, this file specifically, and try to figure out what can cause it to attempt a graceful shutdown and exit with -1 status.

ZakiPathan2010 commented 1 year ago

Hi @birdofpreyru , so here is what I’m trying to do in my sample project. This project has nothing else to make sure there is no memory leak from anywhere else. I started the server in app.tsx file (just like in the example code) which is the entry point of my app & I have two screens which navigates to each other without any server code. There is no reference to the server anywhere else in the code except the App.tsx. So what happens here, is that my server is starts as usual & when I navigate from 1st screen to 2nd screen & go back to 1st screen, after a few times the app freezes. In this process the app is not in background & I never closed it.

In my main app too, after starting the server I would have to navigate to other screens as I did in my last example.

I am attaching my project here so you can try it by yourself too. Sample Project Also things to note are: my project is a expo ejected bare React project.

birdofpreyru commented 1 year ago

So, I arrived to briefly look at your sample project now; and only now I realised it is an Expo project, which this library does not yet support officially. I tried to build by first removing artifacts from your builds you packed into the archive, and then doing npm install; npm run android — that breaks for me somewhere in the middle of the build. I guess, it requires some extra Expo-specific actions.

Thus, I guess this issue will be on hold until I decide to support Expo. Before that, I can just re-iterate, for me the server works fine on Android with a regular RN project (see the example app in the repo); and stay on look-out for possible Android-specific issues.

birdofpreyru commented 1 month ago

Usage in React Native: ...

Tells who? Some kind of Artificial Idiocy?

NanoHttpd: Commonly used in React Native, particularly for serving local files within the application. This is because it can be easily embedded and runs efficiently within the Android environment.

  1. HanoHttpd was a Java-only server; and React Native is at least Android, iOS, macOS, Windows.
  2. The last release of NanoHttpd was 8 years ago, with no further development seen in their repo.

These are two main reasons why this library is powered by Lighttpd — the same, actively maintained server across all platforms.

That's why our server was getting killed and it was crashing.

Nah... most probably it crashes because you do something wrong in your code; or there might be some bug in the library somewhere.

gstrauss commented 1 month ago

@jyotipixolo

That's why our server was getting killed and it was crashing.

Maybe I missed it, but you provided no evidence for that statement. Just an unsubstantiated conclusion.

If you can trace the process or thread which starts lighttpd using strace, that may help identify what system call is failing and resulting in lighttpd shutting down. (I don't know if this is possible in the Android debug env)

[note] graceful shutdown started suggests that the lighttpd thread received SIGINT (lighttpd graceful shutdown) or SIGUSR1 (lighttpd graceful restart). If that was not intended, then maybe your app should block signals in the lighttpd thread so that signals are not delivered to the lighttpd thread, e.g. sigprocmask(). That might be a good idea if the java app manages the lifetime of the lighttpd thread and you do not want/need lighttpd to react to signals.