Debug Python related CRASH



  • hi, I rely heavily on Python Tags for some tasks

    the setup is a bit complex, because the tags import my own modules,
    but it worked reliable for years.

    Now I get freezes / crashes on Windows when I open my file
    and strangely that does not happen on macOS.

    Is there a way to

    1. have cinema4d log all python console output to a file?
      maybe I then can see what causes the crashes

    2. safe start c4d without the execution of python tags in the scene?

    best, index



  • Hi,

    your problem sounds a bit like a violation of Cinema's Threading restrictions, but it is hard to tell without any code.

    1. Not natively, but you can mess around with sys.stdout like in any Python interpreter. If you want to have both a console output and a log, you could use something like I did provide at the end of my post. Please remember that my code below is an example and not meant to be used directly.
    2. Also not possible, afaik.

    Cheers,
    zipit

    Code example for a logger object. You have to save the code to a file for this script to work. Details in the doc string.

    """A very simple logger that writes both to the console and to a file.
    
    Saves this code to a file and run the file from the script manager. Cinema
    will start logging all console output to a file in the same folder as
    this script is located in.
    
    The file will only be finalized / updated when you close Cinema, or when
    you restart the logger, i.e. type the following into the console:
    
        import sys
        sys.stdout.restart()
    
    To 'undo' what this script has done, either restart Cinema or type this
    into the console:
    
        import sys
        sys.stdout = sys.stdout._stdout
    """
    
    import datetime
    import os
    import sys
    
    class ConsoleLogger(object):
        """A very simple logger that writes both to the console and to a file.
    
        The logger uses a dangling file (a file that is being kept open) which 
        is mostly considered a big no-no the days. The safer approach would be
        to use a file context in `write`, but reopening a file for each write
        access comes at a cost.
    
        The purpose of the whole class is to imitate file object so that it can
        be attached to sys.stdout. The relevant methods are ConsoleLogger.write
        and ConsoleLogger.flush. The rest is more or less fluff. 
    
        If you want to be on the safe side, you could also implement the whole
        file object interface, but I wasn't in the mood for doing that ;) 
        """
        def __init__(self):
            """Initializes the logger. 
            """
            self._file = None
            self._stdout = sys.stdout
            self.restart()
    
        def __del__(self):
            """Destructor.
    
            We have to close the file when the logger is getting destroyed. Also
            reverts sys.stdout to its original state upon instantiation.
            """
            sys.stdout = self._stdout
            self._file.close()
    
        def restart(self):
            """Starts a new logger session.
            """
            # Some date-time and file path gymnastics.
            time_stamp = datetime.datetime.now().ctime().replace(":", "-")
            file_path = "{}.log".format(time_stamp)
            file_path = os.path.join(os.path.split(__file__)[0], file_path)
    
            # Close old file and create a new one.
            if self._file is not None:
                self._file.close()
            self._file = open(file_path, "w+")
    
        def close(self):
            """Closes the logger manually.
            """
            self._file.close()
            self._file = None
    
        def write(self, message):
            """Writes a message.
            
            Writes both to the console `sys.stdout` object and the log file.
            
            Args:
                message (str): The message.
            
            Raises:
                IOError: When the log file has been closed.
            """
            self._stdout.write(message)
            if self._file is None:
                msg = "Logger file has been closed"
                raise IOError(msg)
            self._file.write(message)
    
        def flush(self):
            """Flushes sys.stdout.
    
            Needed for Python, you could also flush the file, but that is
            probably not what you want.
            """
            self._stdout.flush()
    
    if __name__ == "__main__":
        sys.stdout = ConsoleLogger()
    


  • thx zipit,

    can i apply that stdout redirection at the root somewhere, so that it applies for all python output?

    somewhere in resource/modules/python/ ...

    as my file crashes c4d on open, i need a temp hack to get that output :)

    index



  • nevermind... i figured it out and added the console logger in

    resource/modules/python/libs/python27/_maxon_init

    to get live output in the file I also added

    self._file.flush()
    

    to the write() method



  • ... the ConsoleLogger works nicely, but it didnt help me finding the bug :-)
    in the end it was this function, executed in a lot (±100) of python tags :

    def userdataFocus(doc, tag):
    	''' set object manager focus on userdata '''
    	doc.SetActiveTag(tag)
    	c4d.gui.ActiveObjectManager_SetObject(c4d.ACTIVEOBJECTMODE_TAG, tag, c4d.ACTIVEOBJECTMANAGER_SETOBJECTS_OPEN, c4d.ID_USERDATA)
    	doc.SetActiveTag(None)
    

    macos can handle that, on windows it leads to a crash.

    I added it, because (most of the time) after opening a file the Python Tags "Tag" tab is active showing the code.
    Actually a single function call would be enough, but how can you achieve that when the tags dont know of each other.

    I'd need to store an info globally which doesn't stay alive when saving / reopening the file.
    is that possible? (store info outside the python tags scope, accessible from all python tags)



  • Hi,

    @indexofrefraction said in Debug Python related CRASH:

    I'd need to store an info globally which doesn't stay alive when saving / reopening the file.

    1. To store stuff 'globally' the common approach is to inject your objects into the Python interpreter. There are many places you can choose from and they are all equally bad. It's a hack. I like to use Python's module dictionary, because it has the benefit of a nice syntax (you can import your stuff). Just make sure not to overwrite any modules.
    import sys
    
    if "my_stuff" not in sys.modules.keys():
        sys.modules["my_stuff"] = "bob is your uncle!"
    
    # Somewhere else in the same interpreter instance.
    import my_stuff
    print my_stuff
    
    1. The life time of objects depends on their document being loaded. So if you want to 'cache' nodes beyond the life time of their hosting document, you have to do that manually. Below you will find a rough sketch on how you can approach that. There are many other ways to approach that. The UUIDs for a document will work beyond save boundaries, so you can cache something with that approach, save the document, even relocate it, and then load it again and feed the document into the cache interface query and will spit out the clones of the cached nodes.

    Cheers,
    zipit

    """A simple interface to cache nodes.
    
    This is an example, not finished code.
    
    Run in the script manager, read the comments. The *meat* is in
    CacheInterface.add() and CacheInterface.query(), start reading in main().
    """
    
    import c4d
    
    class CacheInterface (object):
        """A simple interface to cache nodes.
        """
        def __init__(self):
            """Init.
            """
            self._cache = {}
    
        def _get_cache(self, uuid):
            """Returns the cache for a cache document identifier.
    
            Args:
                uuid (c4d.storage.ByteSequence): The document identifier for the
                 cache.
            
            Returns:
                dict["doc": c4d.documents.BaseDocument, 
                     "nodes": list[c4d.GeListNode]]: The cache for the given 
                     identifier.
            """
            if not uuid in self._cache.keys():
                doc = c4d.documents.BaseDocument()
                self._cache[uuid] = {"doc": doc, "nodes": []}
            return self._cache[uuid]
    
        def _get_tag_index(self, node, tag):
            """Returns the index of a tag.
    
            Args:
                node (c4d.BaseObject): The object the tag is attached to.
                tag (c4d.BaseTag): The tag.
            
            Returns:
                int or None: The index or None when the tag is not attached to
                 the object.
            """
            current = node.GetFirstTag()
            index = 0
            while current:
                if current == tag:
                    return index
                current = current.GetNext()
            return None
    
        def add(self, node):
            """Adds a node to the cache.
    
            Args:
                node (c4d-GeListNode): The node to cache. Currently only 
                BaseObjects and BaseTags have been implemented.
            
            Returns:
                c4d-storage.ByteSeqeunce: The identifier for the cache the node
                has been inserted in.
            
            Raises:
                NotImplementedError: When an unimplemented node type has been 
                passed.
                ValueError: When the passed node is not attached to a document.
            """
            doc = node.GetDocument()
            if doc is None:
                raise ValueError("Can only cache nodes attached to a document.")
    
            uuid = doc.FindUniqueID(c4d.MAXON_CREATOR_ID)
            cache = self._get_cache(uuid)
    
            # BaseObjects
            if isinstance(node, c4d.BaseObject):
                # Pretty easy, clone and inject
                clone = node.GetClone(0)
                cache["doc"].InsertObject(clone)
                cache["nodes"].append(clone)
            # BaseTags
            elif isinstance(node, c4d.BaseTag):
                # For tags things are a bit more complicated.
                # First we get the index of the tag. 
                index = self._get_tag_index(node.GetObject(), node)
                if index is None:
                    msg = "Could not find tag on source object."
                    raise AttributeError(msg)
    
                # Then we clone the host of object of the tag and the tag.
                clone_object = node.GetObject().GetClone(0)
                clone_tag = clone_object.GetTag(node.GetType(), index)
                # Insert the host object into our cache document.
                cache["doc"].InsertObject(clone_object)
                # And append the tag of the same type at the given index
                # to our cache list. A bit dicey, you might wanna add some
                # checks to ensure that we actually got the right tag.
                cache["nodes"].append(clone_tag)
            # Other node types go here ...
            else:
                msg = "The node type {} has not been implemented for caching."
                raise NotImplementedError(msg.format(type(node)))
            return uuid
    
        def query(self, identifier):
            """
            Args:
                identifier (c4d.storage.ByteSeqence | 
                c4d.documents.BaseDocument): The identifier for the cache to
                query or the document that has been the source for the cache.
            
            Returns:
                list[c4d.GeListNode]: The cached nodes.
            
            Raises:
                KeyError: When the provided identifier does not resolve.
                TypeError: When the identifier is neither a document nor an uuid.
            """
            if isinstance(identifier, c4d.documents.BaseDocument):
                identifier = identifier.FindUniqueID(c4d.MAXON_CREATOR_ID)
    
            if not isinstance(identifier, c4d.storage.ByteSeq):
                msg = "Illegal identifier type: {}"
                raise TypeError(msg.format(type(uuid)))
    
            uuid = identifier
            if uuid not in self._cache.keys():
                msg = "Provided uuid is not a key in the cache."
                raise KeyError(msg)
    
            return self._cache[uuid]["nodes"]
    
    def main():
        """Entry point.
        """
        if op is None:
            raise ValueError("Please select an object to cache.")
        # The cache
        interface = CacheInterface()
        # Some stuff to cache
        obj, tag = op, op.GetFirstTag()
        # Inject the nodes into the cache.
        uuid = interface.add(obj)
        # We do not need to store the uuid here, since both nodes come from
        # the same document.
        interface.add(tag)
    
        print "Original nodes:", obj, tag
        print "Their cache:", interface.query(uuid)
    
    if __name__ == "__main__":
        main()
    


  • hi zipit,

    thank u a lot for those tips ! :)

    about the CacheInterface i dont know if i do that right,
    but if i select a cube and execute in the script manager
    R20 crashes right away .-)



  • Hi,

    that is a bit weird. I had ran the example code above in R21 without any crashes. The code is also quite tame, there isn't anything in there which I would see as an obvious candidate for conflicts, as it does not mess with scene or something like that.

    Could you elaborate under which circumstances the code above produces a crash?

    Cheers,
    zipit



  • Hi @indexofrefraction glad you found the cause of the issue, however I'm wondering in which context

    def userdataFocus(doc, tag):
    	''' set object manager focus on userdata '''
    	doc.SetActiveTag(tag)
    	c4d.gui.ActiveObjectManager_SetObject(c4d.ACTIVEOBJECTMODE_TAG, tag, c4d.ACTIVEOBJECTMANAGER_SETOBJECTS_OPEN, c4d.ID_USERDATA)
    	doc.SetActiveTag(None)
    

    Is called.

    Since it does crash when you open the file directly, I assume you have the previous code called during the Execute of a Tag or a Python Generator.
    But both functions are threaded functions. Meaning Any GUI related call is not allowed, because as you experienced, doing GUI stuff within a thread can make Cinema 4D crash for more information please read threading information

    This could also confirm why it works on Mac OS and not on Windows since they are 2 different OS where the drawing pipeline is slightly different so you may found some cases where something does work in one Os but not on the other and vise-versa.

    Cheers,
    Maxime.



  • @zipit

    just FYI, the CacheInterface immediately crashes R20 but works on R21

    @m_adam

    Ok, thanks!
    I was doing this in every Python Tag and I have 100-200 of those in the File.
    I don't really need to do this, it was just a "beauty" thing... that did end up ugly :-D


Log in to reply