omz:forum

    • Register
    • Login
    • Search
    • Recent
    • Popular

    Welcome!

    This is the community forum for my apps Pythonista and Editorial.

    For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.


    Prevent duplicate launch from shortcut

    Pythonista
    home shortcuts
    6
    18
    10186
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • shinyformica
      shinyformica last edited by

      Is there a good, canonical way to prevent a script from launching again if there is already an instance running? Specifically, I want to prevent the scenario where a user launches the script from the home screen via an app shortcut, then goes back to the home screen and launches it again.

      Right now that causes duplicate instances of my script to run. My current method is to have the first instance put a small token in its directory which any other instance can look for and immediately return without running if it isn't the owner of that token. The token is removed by the first instance at exit. This requires quite a bit of careful negotiation and has many difficult edge cases...so I would prefer some simpler way to make sure only a single instance of the script can be running at any time.

      JonB 1 Reply Last reply Reply Quote 0
      • JonB
        JonB @shinyformica last edited by

        @shinyformica
        Are you talking from the wrench/play button, or from a url scheme? Presumably you are talking about something with a UI... Fullscreens, panel, sheet or popover?

        Presumably it is sufficient to see if the UI is present in the app's view heirarchy?

        If so the simplest is to use a tag. Code below is from the mapview thread, by omz:

        # ...
        existing_view = toolbar.viewWithTag_(42)
        if existing_view:
          existing_view.removeFromSuperview()
        ObjCInstance(replacement_view).setTag_(42)
        toolbar.addSubview_(replacement_view)
        # ..
        

        In your case you would use different logic upon detecting existing_view, and might use the UIApplication root view, instead of toolbar, or at least the view you know is root to the way you are presenting.

        Also, since the ui module, or anything else in site packages doesn't get cleared from sys.modules, you could store a weakref to your object in ui, then check if it still exists. For sure that works with play and wrench, not 100% sure about url scheme launches.

        If your module is in site-packages, you could also implement a singleton pattern in your class -- storing your instance as a class variable. Your module needs to be in site packages or begin with double underscores (IIRC... See pykit_preflight.py) to survive global clears.

        You could also use a global variable for your instance, as long as the name starts with double underscore, it will not be cleared when you run a new script.

        If you get your instance via python, then you could check the ovjcinstance's superview() to see if it is still in the view heirarchy. (Iirc on_screen returns false for panels when you are in the editor, but the objc superview is pretty solid way to see if it has been closed)

        1 Reply Last reply Reply Quote 0
        • shinyformica
          shinyformica last edited by

          As I said, this is for when the user launches the script via a shortcut on the home screen...so I assume that's a URL scheme type of launch.

          It's a UI, a fullscreen present(). I like the idea of using a singleton pattern, or storing info in the globals that don't clear between runs for it...I'll give that a try first.

          1 Reply Last reply Reply Quote 0
          • JonB
            JonB last edited by

            hmm, it turns out when the view is not on the screen, even for panel, it is removed from the view heirarchy... this is different from what i remember.

            1 Reply Last reply Reply Quote 0
            • shinyformica
              shinyformica last edited by

              Well...I tried a few different approaches to this, mainly looking at this thread with info about how Pythonista clears the global state when a script runs:

              https://forum.omz-software.com/topic/4566/programmable-keys-in-pythonista-for-quickly-wrench-actions-execution/46?page=3

              Unfortunately, none of my approaches worked for when the script is re-launched via a home screen shortcut. In that case, it seems that Pythonista does a harder reset of state, and the running instance loses access to the modules and module-level variables that it had (they all get set to None, if I print the key/value pairs in the globals() dictionary). I can't think of a simple way to prevent that from happening.

              For now, for my purposes, I am simply making my script run by default via the pythonista_startup.py when deployed, which is good enough. I may have to revisit going through the process of making it a "true" app via the pythonista Xcode template.

              1 Reply Last reply Reply Quote 0
              • JonB
                JonB last edited by

                Check if the imports are in sys.modules. usually when globals are cleared, the modules in site-packages are not, but you lose the globals that reference the module.

                The trick is all callback methods need to import any modules within the method itself.

                1 Reply Last reply Reply Quote 0
                • TPO
                  TPO last edited by

                  @shinyformica, I have the same issue and have come up with a solution based on a simple protocol. When applications follow the protocol, they will not "pile up on top of one another" when launched from the iOS home screen:

                  • When an app is already active in Pythonista and it is launched again with its home screen shortcut, the new instance of the app detects the situation and exits, leaving the already active instance of the app on screen.
                  • When an app is launched from its home screen shortcut and a previous app is already active in Pythonista, the previous app will be notified that it should terminate, its main UI view will be closed, and the new app will be launched.

                  Protocol

                  1. An application should create an instance of class AppSingleLaunch, and use it to test if the application is already active, using the is_active() method. If yes, the application should simply exit. If not, the application should declare its main UI view, using the will_present() method, and present the view. Here is an example:

                     import app_single_launch
                    
                     app = AppSingleLaunch("MyApp")
                     if not app.is_active():
                         view = MyAppView(app)
                         app.will_present(view)
                         view.present()
                    
                  2. An application should make a call to the AppSingleLaunch.will_close() method, from the will_close() method of its main UI view:

                     class MyAppView(ui.View):
                    
                         def __init__(self, app):
                             self.app = app
                    
                         def will_close(self):
                           self.app.will_close()
                    

                  Demo

                  Save the code for app_single_launch.py and the two demo apps in the same directory.

                  Define home screen shortcuts for the two demo apps.

                  Launch demo app 1 using its home screen shortcut, type some text in the text field, then relaunch the app using its home screen shortcut : the text typed previously is still showing, meaning we are using the first launched instance, not the second one. Closing the app brings us back to the Pythonista IDE, not to previously piled up instances of the app.

                  Launch demo app 1 using its home screen shortcut, then Launch demo app 2 using its home screen shortcut, then close it : the Pythonista IDE shows, not a piled up instance of demo app 1.

                  Et voilà !

                  app_single_launch.py

                  """ Ensure a Pythonista script can only be launched once from the iOS home screen.
                  
                  This module provides a solution to the problem stated by shinyformica, which I
                  have also encountered (https://forum.omz-software.com/topic/5440/prevent-
                  duplicate-launch-from-shortcut):
                      "Is there a good, canonical way to prevent a script from launching again if
                      there is already an instance running? Specifically, I want to prevent the
                      scenario where a user launches the script from the home screen via an app
                      shortcut, then goes back to the home screen and launches it again.""
                  
                  The solution is based on a simple protocol, which applications need to adhere
                  to. When this is the case, applications will not "pile up on top of one another"
                  when launched from the iOS home screen:
                  - When an app is already active in Pythonista and it is launched again with its
                    home screen shortcut, the new instance of the app detects the situation and
                    exits, leaving the already active instance of the app on screen.
                  - When an app is launched from its home screen shortcut and a previous app is
                    already active in Pythonista, the previous app will be notified that it
                    should terminate, its main UI view will be closed, and the new app will be
                    launched.
                  
                  Protocol:
                  1) An application should create an instance of class AppSingleLaunch, and use
                     it to test if the application is already active, using the is_active()
                     method. If yes, the application should simply exit. If not, the application
                     should declare its main UI view, using the will_present() method, and
                     present the view. Here is an example:
                  
                          import app_single_launch
                  
                          app = AppSingleLaunch("MyApp")
                          if not app.is_active():
                              view = MyAppView(app)
                              app.will_present(view)
                              view.present()
                  
                  2) An application should make a call to the AppSingleLaunch.will_close()
                     method, from the will_close() method of its main UI view:
                  
                          class MyAppView(ui.View):
                  
                              def __init__(self, app):
                                  self.app = app
                  
                              def will_close(self):
                                self.app.will_close()
                  
                  Implementation: in order to achieve the desired result, we need to remember the
                  last application launched according to the protocol, to determine if it is
                  still active, and, if it is, to close it. This is achieved by storing into a
                  lock file some information about the last application launched:
                  - Its name, as passed to single_launch.launching()
                  - The id of the ui.View instance for its main view, as passed to single_launch.
                    launching(). This is later used to determine if the view is still on screen
                    (when an object is still associated with the id), and to close the app's view.
                    After several tests, it turns out we must use an ui.View object for this
                    purpose, as they seem to persist better than other objects after the cleanup
                    pykit-preflight.py does when an app is launched from the home screen.
                  The location of the lock file is defined by global variable LOCK_PATH. The
                  default location is in the 'site-packages' directory.
                  
                  Known issue:
                  - When an app is on screen, then launched again from its home screen shortcut,
                    some issues may happen with inline import statements (rare, would need to be
                    qualified further).
                  
                  26-Feb-2019 TPO - Created this module
                  28-Feb-2019 TPO - Initial release
                   3-Mar-2019 TPO - Wrapped the code into the AppSingleLaunch class """
                  
                  
                  import gc
                  import json
                  from pathlib import Path
                  import time
                  from typing import Any
                  
                  import ui
                  
                  
                  __all__ = [
                      'AppSingleLaunch',
                  ]
                  
                  
                  DEBUG = False
                  LOCK_PATH = '~/Documents/site-packages/single_launch.lock'
                  
                  
                  def _object_for_id(id_: int) -> Any:
                      """ Return an object, given its id. """
                  
                      # Do a complete garbage collect, to avoid false positives in case the
                      # object was still in use recently. In the context of AppSingleLaunch,
                      # this would happen if an app was closed, then launched again immediately.
                      gc.collect()
                      for obj in gc.get_objects():
                          if id(obj) == id_:
                              return obj
                      return None
                  
                  
                  class AppSingleLaunch:
                      """ Wrapper class for all module functionnality. """
                  
                      def __init__(self, app: str):
                          """ Initialize an AppSingleLaunch instance.
                  
                          Arguments:
                          - app: application name, which should be unique (but this is not
                          enforced). """
                          self.app = app
                  
                      def is_active(self) -> bool:
                          """ Test if the application is already active.
                  
                          Returns:
                          - True if the application is already running, in which case the caller
                            should do nothing and exit.
                          - False if the application is not already running, in which case the
                            caller should launch the application in a normal way, and declare its
                            main view by calling the will_present() method."""
                          if DEBUG:
                              print(f"is_active(), app = {self.app}")
                          lock_path = Path(LOCK_PATH).expanduser()
                          if lock_path.exists():
                              with open(lock_path) as lock_file:
                                  (lock_app, lock_view_id) = tuple(json.load(lock_file))
                              lock_view = _object_for_id(lock_view_id)
                              if DEBUG:
                                  print("- Lock file =", lock_app, lock_view_id,
                                        "valid" if lock_view else "invalid")
                              if lock_app == self.app and lock_view:
                                  if DEBUG:
                                      print(f"- App {self.app} already active")
                                  return True
                          if DEBUG:
                              print(f"- App {self.app} not active")
                          return False
                  
                      def will_present(self, view: ui.View) -> None:
                          """ Declare that the application is about to present its main view.
                  
                          Arguments:
                          - view: ui.View instance for the app's main view. """
                          if DEBUG:
                              print(f"will_present({id(view)}), app = {self.app}")
                          lock_path = Path(LOCK_PATH).expanduser()
                          if lock_path.exists():
                              with open(lock_path) as lock_file:
                                  (lock_app, lock_view_id) = tuple(json.load(lock_file))
                              lock_view = _object_for_id(lock_view_id)
                              if DEBUG:
                                  print("- Lock file =", lock_app, lock_view_id,
                                        "valid" if lock_view else "invalid")
                              if lock_app == self.app and lock_view:
                                  raise ValueError(f"App {self.app} is already active, cannot "
                                                   f"call will_present() against it.")
                              else:
                                  if lock_view and isinstance(lock_view, ui.View):
                                      if DEBUG:
                                          print(f"- Closing app {lock_app}")
                                      lock_view.close()
                                      time.sleep(1)  # Required for view to close properly
                                  # else: lock is a leftover from a previous Pythonista session
                                  #       and can be safely ignored.
                          with open(lock_path, 'w') as lock_file:
                              json.dump([self.app, id(view)], lock_file)
                          if DEBUG:
                              print(f"- Launching app {self.app}\n- Lock file =", self.app, id(view))
                  
                      def will_close(self) -> None:
                          """ Declare that the application is about to close its main view. """
                          lock_path = Path(LOCK_PATH).expanduser()
                          if lock_path.exists():
                              with open(lock_path) as lock_file:
                                  (lock_app, lock_view_id) = tuple(json.load(lock_file))
                              if lock_app != self.app:
                                  raise ValueError(f"App {self.app} if not active, "
                                                   f"{lock_app} is active")
                              lock_path.unlink()
                  

                  single_launch_demo1.py

                  """ app_single_launch demo script #1. """
                  from app_single_launch import AppSingleLaunch
                  import ui
                  
                  
                  class MainView(ui.View):
                      def __init__(self, app: AppSingleLaunch):
                          self.app = app
                          self.name = "Demo app 1"
                          self.flex = 'WH'
                          self.background_color = 'white'
                          self.add_subview(ui.TextField(
                              width=200,
                              height=30,
                              placeholder="Type some text"))
                  
                      def will_close(self) -> None:
                          self.app.will_close()
                  
                  
                  if __name__ == '__main__':
                      app = AppSingleLaunch("Demo app 1")
                      if not app.is_active():
                          view = MainView(app)
                          app.will_present(view)
                          view.present()
                  

                  single_launch_demo2.py

                  """ app_single_launch demo script #2. """
                  from app_single_launch import AppSingleLaunch
                  import ui
                  
                  
                  class MainView(ui.View):
                      def __init__(self, app: AppSingleLaunch):
                          self.app = app
                          self.name = "Demo app 2"
                          self.flex = 'WH'
                          self.background_color = 'white'
                          self.add_subview(ui.TextField(
                              width=200,
                              height=30,
                              placeholder="Type some text"))
                  
                      def will_close(self) -> None:
                          self.app.will_close()
                  
                  
                  if __name__ == '__main__':
                      app = AppSingleLaunch("Demo app 2")
                      if not app.is_active():
                          view = MainView(app)
                          app.will_present(view)
                          view.present()
                  
                  nfmusician 1 Reply Last reply Reply Quote 1
                  • shinyformica
                    shinyformica last edited by

                    Just revisiting this issue, since it's once again on my plate to try and find a working solution.

                    When we left off, I'd tried various attempts using different ideas discussed above. None of them are really successful. I can successfully prevent a new instance from trying to launch, keeping a token in the ui module which let's me know if an instance is already running, and which is cleared when the running instance is closed.

                    The trouble is still with what happens when a script is launched via a home screen shortcut.

                    In that case, the pykit_preflight.py script is in some way being run (it's not clear exactly when and how that occurs @omz...any hints?), and it clears out the sys.modules and globals before it runs the script. So even though the newly launched script sees that an instance is already running, and bails immediately, the previous instance is left in a broken state.

                    The clearing of the modules and globals means the existing instance is suddenly left with all of its module-level global data pointing at None, so it ends up raising an exception as soon as it tries to do anything.

                    So...I tried all sorts of ways to prevent or replace how pykit_preflight does it's thing to no avail. I even tried stashing away the globals for every module in my codebase and restoring them all. No luck.

                    Anyone have any ideas or better knowledge of how to prevent the clearing when a script is launched from the home screen shortcut, while pythonista is already open and running?

                    1 Reply Last reply Reply Quote 0
                    • JonB
                      JonB last edited by

                      Iirc, @dgelessus figured out a way to bypass the preflight altogether. I can't find that thread right now, I think it involved hijacking a call that happened within preflight.. this might be an option in his pythonista_startup.

                      You probably know all this, but from another thread, here are some methods to avoid module and or variables getting cleared:

                      ##ways to keep modules from clearing:

                      1. for modules, delete file
                        e.g
                         import mymodule
                         del mymodule.__file__
                      

                      now mymodule is not removed from sys.modules

                      1. create name staring with dunder__
                      import mymodule
                      sys.modules['__mymodule']=sys.modules['mymodule']
                      
                      1. set file access to nonwritable
                      chmod(mymodule.__file__,0xo444)
                      
                      1. place module in site-packages

                      ##ways to keep global vars from being cleared
                      1.start variable with __
                      __myglobalvar=4
                      2.add to pythonista_startup
                      myglobalvar=4
                      import pythonista_startup

                      pythonista_startup.myglobalvar=myglobalvar

                      1. for things like ui.Views, threads, which needs to reference itself, but does not need to be refernced externally, etc it is sufficient to simply add the var to an existing builtin module

                        try:
                        ui.mysavedviews.append(myview)
                        except NameError:
                        ui.mysavedviews=[myview]

                      retain_global is another way.

                      other thoughts

                      For modules getting cleared out from under you, the only practical solution iirc is that every function must import what it needs. That's what stash does, and it can survive anything (I think including pythonista://). It is really tedious-- basically anything that can get called after your initial launch cannot rely on modules being imported at the file/module level. Instead, each def must import modules it uses. There is not a great way to check that you remembered everything.

                      1 Reply Last reply Reply Quote 0
                      • JonB
                        JonB last edited by

                        Take a look at (here](https://github.com/dgelessus/pythonista_startup/blob/master/_preflight_hook_experiment.py)
                        And also
                        Here
                        at line 39
                        The first let's you register hooks that get called very early in preflight. I suppose you could check for your UI, and if present, raise an Exception, thus killing preflight. (I'm not 100% sure that's work).

                        Option 2 uses the DirAllTheGlobals trick to patch pythonista_startup's dir to return everything in main -- since preflight checks if each variable is in pythonista_startup.

                        I'd think you could also create a custom importer, that forced all imports into pythonista_startup where it would be safe, or maybe have a system to "register" required imports/globals associated with an app, that can be cleared when app closes

                        1 Reply Last reply Reply Quote 1
                        • shinyformica
                          shinyformica last edited by

                          @JonB thanks!

                          Amazingly, yanking out just the "disable global clearing" part of that pythonista_startup magic worked to allow relaunch from the home shortcut.

                          So, the ugly tinkering is relatively short: replace the existing pythonista startup module with a new module object containing the globals for the current pythonista_startup module, and which returns all the globals in main when asked for its contents.

                          This will effectively short-circuit the preflight system from clearing out any globals, since it skips any which are inside the pythonista startup module itself.

                          Here's the shortened code just to solve this one issue, if anyone else is encountering it:

                          print(u"Executing pythonista_startup...")
                          
                          try:
                              print(u"Preventing globals clearing...")
                              
                              import sys
                              import types
                              
                              class SaveGlobals(types.ModuleType):
                                  import __main__
                                  
                                  def __dir__(self):
                                      return dir(type(self).__main__)
                              
                              # THESE LINES MUST COME LAST.
                              # Anything past this point is executed in the context of the old
                              # pythonista_startup module, which may already be partially
                              # garbage-collected.
                              saved = SaveGlobals(__name__, __doc__)
                              vars(saved).update(vars(sys.modules["pythonista_startup"]))
                              sys.modules["pythonista_startup"] = saved
                              
                              del sys
                              del types
                              del SaveGlobals
                              del saved
                              
                              print(u"Done preventing globals clearing.")
                          except: # Catch everything
                              import sys
                              import traceback
                              
                              print(u"Swallowed exception:", file=sys.stderr)
                              traceback.print_exc()
                              
                              print(u"Attempt to re-raise", file=sys.stderr)
                              del sys
                              del traceback
                              raise
                          
                          print(u"Done executing pythonista_startup.")
                          
                          jmv38 1 Reply Last reply Reply Quote 0
                          • jmv38
                            jmv38 @shinyformica last edited by

                            @shinyformica hello!
                            how do you make a homescreen shortcut to a pythonista script?
                            thanks!

                            cvp 1 Reply Last reply Reply Quote 0
                            • cvp
                              cvp @jmv38 last edited by cvp

                              @jmv38 here or here

                              jmv38 1 Reply Last reply Reply Quote 0
                              • jmv38
                                jmv38 @cvp last edited by jmv38

                                @cvp thank you!
                                when you say:

                                Altough last version of Pythonista offers an easy way to add an home screen shortcut for an edited script

                                do you mean the first link above?

                                cvp 1 Reply Last reply Reply Quote 0
                                • cvp
                                  cvp @jmv38 last edited by cvp

                                  @jmv38 no, via wrench icon
                                  Problem is that beta expires in 2 days...

                                  jmv38 1 Reply Last reply Reply Quote 0
                                  • jmv38
                                    jmv38 @cvp last edited by

                                    @cvp actually it does work on the regular version.
                                    Not sure why i didnt see that before.
                                    Thanks.

                                    1 Reply Last reply Reply Quote 0
                                    • nfmusician
                                      nfmusician @TPO last edited by

                                      @TPO Do you have this on a Github anywhere? It's useful.

                                      TPO 1 Reply Last reply Reply Quote 0
                                      • TPO
                                        TPO @nfmusician last edited by

                                        @nfmusician there you go : https://github.com/TPO-POMGOM/Pythonista-utilities

                                        (I currently lack time to migrate to Github other utilities and modules I have developped for Pythonista, will do it as soon as time allows)

                                        1 Reply Last reply Reply Quote 0
                                        • First post
                                          Last post
                                        Powered by NodeBB Forums | Contributors