Repository: oppia/oppia

Issue:
Enable Oppia to be installed on Debian/Chromebooks
We don't support bare Debian, so we shouldn't reference it in the installation wiki


Discussion:
@seanlip @U8NWXD please assign this to me.
done!
Thanks @seanlip! I cannot change the title of this issue, maybe because it is in oppia/oppia-web-developer-docs. Could you be able to move it to oppia/oppia? If that's not possible, then I can open a new issue in oppia/oppia and you can delete this one.
@ravi-dalal Done -- though if you still can't change the title, feel free to just leave updates in the comment thread. Thanks!
Thank you @seanlip! I am still not able to update the title, so I will add the details as a comment.
The issue that I faced was with running Oppia development server on Chromebook. Chromebook has a Debian 11 VM for the Linux environment. This Debian VM does not have the directory /dev/disk (I am not sure if all the Debian systems do not have this directory).

The `create_managed_web_browser` method in `servers.py` file in Oppia code base creates a web browser instance after the GAE server startup.  This fails on Chromebook because the code tries to read `/dev/disk` directory, which does not exist  on the Chromebook Linux VM.

The fix is to add a check in the code for the existence of /dev/disk directory.  This will allow Oppia development server to run on Chromebook.

Resolving patch:
diff --git a/scripts/servers.py b/scripts/servers.py
--- a/scripts/servers.py
+++ b/scripts/servers.py
@@ -369,31 +369,35 @@ def managed_redis_server() -> Iterator[psutil.Process]:
 
 def create_managed_web_browser(
     port: int
-) -> Optional[ContextManager[psutil.Process]]:
-    """Returns a context manager for a web browser targeting the given port on
+) -> ContextManager[psutil.Process]:
+    """Returns a ContextManager for a web browser targeting the given port on
     localhost. If a web browser cannot be opened on the current system by Oppia,
-    then returns None instead.
+    then raises an exception.
 
     Args:
         port: int. The port number to open in the web browser.
 
     Returns:
-        context manager|None. The context manager to a web browser window, or
-        None if the current operating system does not support web browsers.
+        ContextManager. The ContextManager to a web browser window if the
+        current operating system can be identified and a web browser can be
+        launched automatically.
+
+    Raises:
+        Exception. Unable to launch the web browser (this happens when the
+            Operating System cannot be identified).
     """
     url = 'http://localhost:%s/' % port
     human_readable_name = 'Web Browser'
     if common.is_linux_os():
-        if any(re.match('.*VBOX.*', d) for d in os.listdir('/dev/disk/by-id/')):
-            return None
-        else:
-            return managed_process(
-                ['xdg-open', url], human_readable_name=human_readable_name)
+        return managed_process(
+            ['xdg-open', url], human_readable_name=human_readable_name)
     elif common.is_mac_os():
         return managed_process(
             ['open', url], human_readable_name=human_readable_name)
     else:
-        return None
+        raise Exception(
+            'Unable to identify the Operating System and therefore, unable to '
+            'launch the web browser.')
 
 
 @contextlib.contextmanager
diff --git a/scripts/start.py b/scripts/start.py
--- a/scripts/start.py
+++ b/scripts/start.py
@@ -196,11 +196,7 @@ def main(args: Optional[Sequence[str]] = None) -> None:
             )
             initializer.populate_debug_data()
 
-        managed_web_browser = (
-            None if parsed_args.no_browser else
-            servers.create_managed_web_browser(PORT_NUMBER_FOR_GAE_SERVER))
-
-        if managed_web_browser is None:
+        if parsed_args.no_browser:
             common.print_each_string_after_two_new_lines([
                 'INFORMATION',
                 'Local development server is ready! You can access it by '
@@ -208,13 +204,27 @@ def main(args: Optional[Sequence[str]] = None) -> None:
                 'browser.' % PORT_NUMBER_FOR_GAE_SERVER,
             ])
         else:
-            common.print_each_string_after_two_new_lines([
-                'INFORMATION',
-                'Local development server is ready! Opening a default web '
-                'browser window pointing to it: '
-                'http://localhost:%s/' % PORT_NUMBER_FOR_GAE_SERVER,
-            ])
-            stack.enter_context(managed_web_browser)
+            try:
+                stack.enter_context(servers.create_managed_web_browser(
+                    PORT_NUMBER_FOR_GAE_SERVER))
+                common.print_each_string_after_two_new_lines([
+                    'INFORMATION',
+                    'Local development server is ready! Opening a default web '
+                    'browser window pointing to it: '
+                    'http://localhost:%s/' % PORT_NUMBER_FOR_GAE_SERVER,
+                ])
+            except Exception as error:
+                common.print_each_string_after_two_new_lines([
+                    'ERROR',
+                    'Error occurred while attempting to automatically launch '
+                    'the web browser: %s' % error,
+                ])
+                common.print_each_string_after_two_new_lines([
+                    'INFORMATION',
+                    'Local development server is ready! You can access it by '
+                    'navigating to http://localhost:%s/ in a web '
+                    'browser.' % PORT_NUMBER_FOR_GAE_SERVER,
+                ])
 
         dev_appserver.wait()
 


Test patch:
diff --git a/scripts/servers_test.py b/scripts/servers_test.py
--- a/scripts/servers_test.py
+++ b/scripts/servers_test.py
@@ -618,6 +618,7 @@ def test_managed_web_browser_on_linux_os(self) -> None:
 
         managed_web_browser = servers.create_managed_web_browser(123)
         self.assertIsNotNone(managed_web_browser)
+
         assert managed_web_browser is not None
         self.exit_stack.enter_context(managed_web_browser)
 
@@ -625,17 +626,6 @@ def test_managed_web_browser_on_linux_os(self) -> None:
         self.assertEqual(
             popen_calls[0].program_args, ['xdg-open', 'http://localhost:123/'])
 
-    def test_managed_web_browser_on_virtualbox_os(self) -> None:
-        popen_calls = self.exit_stack.enter_context(self.swap_popen())
-        self.exit_stack.enter_context(self.swap(common, 'OS_NAME', 'Linux'))
-        self.exit_stack.enter_context(self.swap_to_always_return(
-            os, 'listdir', value=['VBOX-123']))
-
-        managed_web_browser = servers.create_managed_web_browser(123)
-        self.assertIsNone(managed_web_browser)
-
-        self.assertEqual(len(popen_calls), 0)
-
     def test_managed_web_browser_on_mac_os(self) -> None:
         popen_calls = self.exit_stack.enter_context(self.swap_popen())
         self.exit_stack.enter_context(self.swap(common, 'OS_NAME', 'Darwin'))
@@ -654,11 +644,32 @@ def test_managed_web_browser_on_mac_os(self) -> None:
     def test_managed_web_browser_on_windows_os(self) -> None:
         popen_calls = self.exit_stack.enter_context(self.swap_popen())
         self.exit_stack.enter_context(self.swap(common, 'OS_NAME', 'Windows'))
-        self.exit_stack.enter_context(self.swap_to_always_return(
-            os, 'listdir', value=[]))
 
-        managed_web_browser = servers.create_managed_web_browser(123)
-        self.assertIsNone(managed_web_browser)
+        with self.assertRaisesRegex(
+            Exception,
+            'Unable to identify the Operating System and therefore, unable to '
+            'launch the web browser.'
+            ):
+            self.exit_stack.enter_context(
+                servers.create_managed_web_browser(123))
+
+        self.assertEqual(len(popen_calls), 0)
+
+    def test_managed_web_browser_for_exception(self) -> None:
+        web_browser_error = 'Mock Exception while launching web browser.'
+        popen_calls = self.exit_stack.enter_context(self.swap_popen())
+        self.exit_stack.enter_context(self.swap(common, 'OS_NAME', 'Linux'))
+        mock_create_managed_web_browser = self.swap_to_always_raise(
+                    servers, 'create_managed_web_browser',
+                    Exception(web_browser_error))
+
+        with mock_create_managed_web_browser:
+            with self.assertRaisesRegex(
+                Exception,
+                web_browser_error
+            ):
+                self.exit_stack.enter_context(
+                    servers.create_managed_web_browser(123))
 
         self.assertEqual(len(popen_calls), 0)
 
diff --git a/scripts/start_test.py b/scripts/start_test.py
--- a/scripts/start_test.py
+++ b/scripts/start_test.py
@@ -26,6 +26,7 @@
 from scripts import servers
 
 PORT_NUMBER_FOR_GAE_SERVER = 8181
+MANAGED_WEB_BROWSER_ERROR = 'Mock Exception while launching web browser.'
 
 
 class MockCompiler:
@@ -104,6 +105,9 @@ def mock_context_manager() -> MockCompilerContextManager:
             servers, 'create_managed_web_browser',
             lambda _: MockCompilerContextManager(),
             expected_args=((PORT_NUMBER_FOR_GAE_SERVER,),))
+        self.swap_create_managed_web_browser = self.swap_to_always_raise(
+            servers, 'create_managed_web_browser',
+            Exception(MANAGED_WEB_BROWSER_ERROR))
 
     def test_start_servers_successfully(self) -> None:
         with self.swap_install_third_party_libs:
@@ -280,3 +284,39 @@ def test_start_servers_successfully_in_contributor_dashboard_debug_mode(
                 )
             ],
             self.print_arr)
+
+    def test_could_not_auto_launch_web_browser(self) -> None:
+        with self.swap_install_third_party_libs:
+            from scripts import start
+        swap_build = self.swap_with_checks(
+            build, 'main', lambda **unused_kwargs: None,
+            expected_kwargs=[{'args': []}])
+
+        with self.swap_cloud_datastore_emulator, self.swap_ng_build, swap_build:
+            with self.swap_elasticsearch_dev_server, self.swap_redis_server:
+                with self.swap_create_managed_web_browser:
+                    with self.swap_webpack_compiler, self.swap_dev_appserver:
+                        with self.swap_extend_index_yaml, self.swap_print:
+                            with self.swap_firebase_auth_emulator:
+                                start.main(args=[])
+
+        self.assertIn(
+            [
+                'ERROR',
+                (
+                    'Error occurred while attempting to automatically launch '
+                    'the web browser: %s' % MANAGED_WEB_BROWSER_ERROR
+                )
+            ],
+            self.print_arr)
+
+        self.assertIn(
+            [
+                'INFORMATION',
+                (
+                    'Local development server is ready! You can access it by '
+                    'navigating to http://localhost:%s/ in a web '
+                    'browser.' % PORT_NUMBER_FOR_GAE_SERVER
+                )
+            ],
+            self.print_arr)