Skip to content

UnrealEngine5

UnrealEngineCmdManagedProcess

Bases: ManagedProcess

Process for executing unreal over commandline

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
class UnrealEngineCmdManagedProcess(ManagedProcess):
    """
    Process for executing unreal over commandline
    """

    def __init__(self, deadline_plugin, process_name, startup_dir=""):
        """
        Constructor
        :param process_name: The name of this process
        """
        if sys.version_info.major == 3:
            super().__init__()
        self._deadline_plugin = deadline_plugin
        self._name = process_name
        self.ExitCode = -1
        self._startup_dir = startup_dir
        self._executable_path = None

        self.InitializeProcessCallback += self._initialize_process
        self.RenderExecutableCallback += self._render_executable
        self.RenderArgumentCallback += self._render_argument
        self.CheckExitCodeCallback += self._check_exit_code
        self.StartupDirectoryCallback += self._startup_directory

    def clean_up(self):
        """
        Called when the plugin cleanup is called
        """
        self._deadline_plugin.LogInfo("Executing managed process cleanup.")
        # Clean up stdout handler callbacks.
        for stdoutHandler in self.StdoutHandlers:
            del stdoutHandler.HandleCallback

        del self.InitializeProcessCallback
        del self.RenderExecutableCallback
        del self.RenderArgumentCallback
        del self.CheckExitCodeCallback
        del self.StartupDirectoryCallback
        self._deadline_plugin.LogInfo("Managed Process Cleanup Finished.")

    def _initialize_process(self):
        """
        Called by Deadline to initialize the process.
        """
        self._deadline_plugin.LogInfo(
            "Executing managed process Initialize Process."
        )

        # Set the ManagedProcess specific settings.
        self.PopupHandling = True
        self.StdoutHandling = True
        self.HideDosWindow = True

        # Ensure child processes are killed and the parent process is
        # terminated on exit
        self.UseProcessTree = True
        self.TerminateOnExit = True

        shell = self._deadline_plugin.GetPluginInfoEntryWithDefault("Shell", "")

        if shell:
            self._shell = shell

        self.AddStdoutHandlerCallback(
            ".*Progress: (\d+)%.*"
        ).HandleCallback += self._handle_progress

        # self.AddStdoutHandlerCallback("LogPython: Error:.*").HandleCallback += self._handle_stdout_error

        # Get the current frames for the task
        current_task_frames = self._deadline_plugin.GetCurrentTask().TaskFrameString

        # Set the frames sting as an environment variable
        self.SetEnvironmentVariable("CURRENT_RENDER_FRAMES", current_task_frames)

    def _handle_stdout_error(self):
        """
        Callback for when a line of stdout contains an ERROR message.
        """
        self._deadline_plugin.FailRender(self.GetRegexMatch(0))

    def _check_exit_code(self, exit_code):
        """
        Returns the process exit code
        :param exit_code:
        :return:
        """
        self.ExitCode = exit_code

    def _startup_directory(self):
        """
        Startup directory
        """
        return self._startup_dir

    def _handle_progress(self):
        """
        Handles progress reports
        """
        progress = float(self.GetRegexMatch(1))
        self._deadline_plugin.SetProgress(progress)

    def _render_executable(self):
        """
        Get the render executable
        """

        self._deadline_plugin.LogInfo("Setting up Render Executable")

        executable = self._deadline_plugin.GetEnvironmentVariable("UnrealExecutable")

        if not executable:
            executable = self._deadline_plugin.GetPluginInfoEntry("Executable")

        # Get the executable from the plugin
        executable = RepositoryUtils.CheckPathMapping(executable)
        # Get the project root path
        project_root = self._deadline_plugin.GetProcessEnvironmentVariable(
            "ProjectRoot"
        )

        # Resolve any `{ProjectRoot}` tokens in the environment
        if project_root:
            executable = executable.format(ProjectRoot=project_root)

        if not FileUtils.FileExists(executable):
            self._deadline_plugin.FailRender(
                "{executable} could not be found".format(executable=executable)
            )

        # TODO: Setup getting executable from the config as well

        self._deadline_plugin.LogInfo(
            "Render Executable: {exe}".format(exe=executable)
        )
        self._executable_path = executable.replace("\\", "/")

        return self._executable_path

    def _render_argument(self):
        """
        Get the arguments to startup unreal
        :return:
        """
        self._deadline_plugin.LogInfo("Setting up Render Arguments")

        # Look for any unreal uproject paths in the process environment. This
        # assumes a previous process resolves a uproject path and makes it
        # available.
        project_file = self._deadline_plugin.GetEnvironmentVariable("UnrealUProject")

        if not project_file:
            project_file = self._deadline_plugin.GetPluginInfoEntry("ProjectFile")

        # Get any path mappings required. Expects this to be a full path
        project_file = RepositoryUtils.CheckPathMapping(project_file)

        # Get the project root path
        project_root = self._deadline_plugin.GetProcessEnvironmentVariable(
            "ProjectRoot"
        )

        # Resolve any `{ProjectRoot}` tokens in the environment
        if project_root:
            project_file = project_file.format(ProjectRoot=project_root)

        if not project_file:
            self._deadline_plugin.FailRender(
                f"Expected project file but found `{project_file}`"
            )

        project_file = Path(project_file.replace("\u201c", '"').replace(
            "\u201d", '"'
        ).replace("\\", "/"))

        # Check to see if the Uproject is a relative path
        if str(project_file).replace("\\", "/").startswith("../"):

            if not self._executable_path:
                self._deadline_plugin.FailRender("Could not find executable path to resolve relative path.")

            # Find executable root
            import re
            engine_dir = re.findall("([\s\S]*.Engine)", self._executable_path)
            if not engine_dir:
                self._deadline_plugin.FailRender("Could not find executable Engine directory.")

            executable_root = Path(engine_dir[0]).parent

            # Resolve editor relative paths
            found_paths = sorted(executable_root.rglob(str(project_file).replace("\\", "/").strip("../")))

            if not found_paths or len(found_paths) > 1:
                self._deadline_plugin.FailRender(
                    f"Found multiple uprojects relative to the root directory. There should only be one when a relative path is defined."
                )

            project_file = found_paths[0]
        self._deadline_plugin.LogInfo(f"project_file:: `{project_file}`")
        # make sure the project exists
        if not FileUtils.FileExists(project_file.as_posix()):
            self._deadline_plugin.FailRender(f"Could not find `{project_file.as_posix()}`")

        # Get the render arguments
        args = RepositoryUtils.CheckPathMapping(
            self._deadline_plugin.GetPluginInfoEntry(
                "CommandLineArguments"
            ).strip()
        )

        args = args.replace("\u201c", '"').replace("\u201d", '"')

        startup_args = " ".join(
            [
                '"{u_project}"'.format(u_project=project_file.as_posix()),
                args,
                "-log",
                "-unattended",
                "-stdout",
                "-allowstdoutlogverbosity",
            ]
        )

        self._deadline_plugin.LogInfo(
            "Render Arguments: {args}".format(args=startup_args)
        )

        return startup_args

__init__(deadline_plugin, process_name, startup_dir='')

Constructor :param process_name: The name of this process

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
def __init__(self, deadline_plugin, process_name, startup_dir=""):
    """
    Constructor
    :param process_name: The name of this process
    """
    if sys.version_info.major == 3:
        super().__init__()
    self._deadline_plugin = deadline_plugin
    self._name = process_name
    self.ExitCode = -1
    self._startup_dir = startup_dir
    self._executable_path = None

    self.InitializeProcessCallback += self._initialize_process
    self.RenderExecutableCallback += self._render_executable
    self.RenderArgumentCallback += self._render_argument
    self.CheckExitCodeCallback += self._check_exit_code
    self.StartupDirectoryCallback += self._startup_directory

clean_up()

Called when the plugin cleanup is called

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
def clean_up(self):
    """
    Called when the plugin cleanup is called
    """
    self._deadline_plugin.LogInfo("Executing managed process cleanup.")
    # Clean up stdout handler callbacks.
    for stdoutHandler in self.StdoutHandlers:
        del stdoutHandler.HandleCallback

    del self.InitializeProcessCallback
    del self.RenderExecutableCallback
    del self.RenderArgumentCallback
    del self.CheckExitCodeCallback
    del self.StartupDirectoryCallback
    self._deadline_plugin.LogInfo("Managed Process Cleanup Finished.")

UnrealEngineManagedProcess

Bases: ManagedProcess

Process for executing and managing an unreal jobs.

.. note::

Although this process can auto start a batch process by
executing a script on startup, it is VERY important the command
that is executed on startup makes a connection to the Deadline RPC
server.
This will allow Deadline to know a task is running and will wait
until the task is complete before rendering the next one. If this
is not done, Deadline will assume something went wrong with the
process and fail the job after a few minutes. It is also VERY
critical the Deadline process is told when a task is complete, so
it can move on to the next one. See the Deadline RPC manager on how
this communication system works.
The reason for this complexity is, sometimes an unreal project can
take several minutes to load, and we only want to bare the cost of
that load time once between tasks.
Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
class UnrealEngineManagedProcess(ManagedProcess):
    """
    Process for executing and managing an unreal jobs.

    .. note::

        Although this process can auto start a batch process by
        executing a script on startup, it is VERY important the command
        that is executed on startup makes a connection to the Deadline RPC
        server.
        This will allow Deadline to know a task is running and will wait
        until the task is complete before rendering the next one. If this
        is not done, Deadline will assume something went wrong with the
        process and fail the job after a few minutes. It is also VERY
        critical the Deadline process is told when a task is complete, so
        it can move on to the next one. See the Deadline RPC manager on how
        this communication system works.
        The reason for this complexity is, sometimes an unreal project can
        take several minutes to load, and we only want to bare the cost of
        that load time once between tasks.

    """

    def __init__(self, process_name, deadline_plugin, deadline_rpc_manager):
        """
        Constructor
        :param process_name: The name of this process
        :param deadline_plugin: An instance of the plugin
        :param deadline_rpc_manager: An instance of the rpc manager
        """
        if sys.version_info.major == 3:
            super().__init__()
        self.InitializeProcessCallback += self._initialize_process
        self.RenderExecutableCallback += self._render_executable
        self.RenderArgumentCallback += self._render_argument
        self._deadline_plugin = deadline_plugin
        self._deadline_rpc_manager = deadline_rpc_manager
        self._temp_rpc_client = None
        self._name = process_name
        self._executable_path = None

        # Elapsed time to check for connection
        self._process_wait_time = int(self._deadline_plugin.GetConfigEntryWithDefault("RPCWaitTime", "300"))

    def clean_up(self):
        """
        Called when the plugin cleanup is called
        """
        self._deadline_plugin.LogInfo("Executing managed process cleanup.")
        # Clean up stdout handler callbacks.
        for stdoutHandler in self.StdoutHandlers:
            del stdoutHandler.HandleCallback

        del self.InitializeProcessCallback
        del self.RenderExecutableCallback
        del self.RenderArgumentCallback
        self._deadline_plugin.LogInfo("Managed Process Cleanup Finished.")

    def _initialize_process(self):
        """
        Called by Deadline to initialize the process.
        """
        self._deadline_plugin.LogInfo(
            "Executing managed process Initialize Process."
        )

        # Set the ManagedProcess specific settings.
        self.PopupHandling = False
        self.StdoutHandling = True

        # Set the stdout handlers.

        self.AddStdoutHandlerCallback(
            "LogPython: Error:.*"
        ).HandleCallback += self._handle_stdout_error
        self.AddStdoutHandlerCallback(
            "Warning:.*"
        ).HandleCallback += self._handle_stdout_warning

        logs_dir = self._deadline_plugin.GetPluginInfoEntryWithDefault(
            "LoggingDirectory", ""
        )
        # error handler for Apple ProRes Media not writing file
        self.AddStdoutHandlerCallback(
            ".*LogAppleProResMedia: Error: Failed to.*"
        ).HandleCallback += self._handle_stdout_error

        self.AddStdoutHandlerCallback(
            ".*LogWindows: FPlatformMisc::RequestExitWithStatus\(1,.*"
        ).HandleCallback += self._handle_stdout_error

        self.AddStdoutHandlerCallback(
            ".*with error DXGI_ERROR_DEVICE_REMOVED with Reason: DXGI_ERROR_DEVICE_HUNG*"
        ).HandleCallback += self._handle_stdout_error

        if logs_dir:

            job = self._deadline_plugin.GetJob()

            log_file_dir = os.path.join(
                job.JobName,
                f"{job.JobSubmitDateTime.ToUniversalTime()}".replace(" ", "-"),
            )

            if not os.path.exists(log_file_dir):
                os.makedirs(log_file_dir)

            # If a log directory is specified, this may redirect stdout to the
            # log file instead. This is a builtin Deadline behavior
            self.RedirectStdoutToFile(
                os.path.join(
                    log_file_dir,
                    f"{self._deadline_plugin.GetSlaveName()}_{datetime.now()}.log".replace(" ", "-")
                )
            )

    def _handle_std_out(self):
        self._deadline_plugin.LogInfo(self.GetRegexMatch(0))

    # Callback for when a line of stdout contains a WARNING message.
    def _handle_stdout_warning(self):
        self._deadline_plugin.LogWarning(self.GetRegexMatch(0))

    # Callback for when a line of stdout contains an ERROR message.
    def _handle_stdout_error(self):
        self._deadline_plugin.FailRender(self.GetRegexMatch(0))

    def render_task(self):
        """
        Render a task
        """

        # Fail the render is we do not have a manager running
        if not self._deadline_rpc_manager:
            self._deadline_plugin.FailRender("No rpc manager was running!")

        # Start a timer to monitor the process time
        start_time = time.time()

        # Get temp client connection
        if not self._temp_rpc_client:
            self._temp_rpc_client = self._deadline_rpc_manager.get_temporary_client_proxy()


        print("Is server and client connected?", self._temp_rpc_client.is_connected())

        # Make sure we have a manager running, and we can establish a connection
        if not self._temp_rpc_client.is_connected():
            # Wait for a connection. This polls the server thread till an
            # unreal process client has connected. It is very important that
            # a connection is established by the client to allow this process
            # to execute.
            while round(time.time() - start_time) <= self._process_wait_time:
                try:
                    # keep checking to see if a client has connected
                    if self._temp_rpc_client.is_connected():
                        self._deadline_plugin.LogInfo(
                            "Client connection established!!"
                        )
                        break
                except Exception:
                    pass

                self._deadline_plugin.LogInfo("Waiting on client connection..")
                self._deadline_plugin.FlushMonitoredManagedProcessStdout(
                    self._name
                )
                time.sleep(2)
            else:

                # Fail the render after waiting too long
                self._deadline_plugin.FailRender(
                    "A connection was not established with an unreal process"
                )

        # if we are connected, wait till the process task is marked as
        # complete.
        while not self._temp_rpc_client.is_task_complete(
            self._deadline_plugin.GetCurrentTaskId()
        ):
            # Keep flushing stdout
            self._deadline_plugin.FlushMonitoredManagedProcessStdout(self._name)

        # Flush one last time
        self._deadline_plugin.FlushMonitoredManagedProcessStdout(self._name)

    def _render_executable(self):
        """
        Get the render executable
        """
        self._deadline_plugin.LogInfo("Setting up Render Executable")

        executable = self._deadline_plugin.GetEnvironmentVariable("UnrealExecutable")

        if not executable:
            executable = self._deadline_plugin.GetPluginInfoEntry("Executable")

        # Resolve any path mappings required
        executable = RepositoryUtils.CheckPathMapping(executable)

        project_root = self._deadline_plugin.GetEnvironmentVariable("ProjectRoot")

        # If a project root is specified in the environment, it is assumed a
        # previous process resolves the root location of the executable and
        # presents it in the environment.
        if project_root:
            # Resolve any `{ProjectRoot}` tokens present in the executable path
            executable = executable.format(ProjectRoot=project_root)

        # Make sure the executable exists
        if not FileUtils.FileExists(executable):
            self._deadline_plugin.FailRender(f"Could not find `{executable}`")

        self._executable_path = executable.replace("\\", "/")

        self._deadline_plugin.LogInfo(f"Found executable `{executable}`")

        return self._executable_path

    def _render_argument(self):
        """
        Get the arguments to startup unreal
        """
        self._deadline_plugin.LogInfo("Setting up Render Arguments")

        # Look for any unreal uproject paths in the process environment. This
        # assumes a previous process resolves a uproject path and makes it
        # available.
        uproject = self._deadline_plugin.GetEnvironmentVariable("UnrealUProject")

        if not uproject:
            uproject = self._deadline_plugin.GetPluginInfoEntry("ProjectFile")

        # Get any path mappings required. Expects this to be a full path
        uproject = RepositoryUtils.CheckPathMapping(uproject)

        # Get the project root path
        project_root = self._deadline_plugin.GetEnvironmentVariable("ProjectRoot")

        # Resolve any `{ProjectRoot}` tokens in the environment
        if project_root:
            uproject = uproject.format(ProjectRoot=project_root)

        uproject = Path(uproject.replace("\\", "/"))
        self._deadline_plugin.LogInfo(f"uproject:: `{uproject}`")
        # Check to see if the Uproject is a relative path
        if str(uproject).replace("\\", "/").startswith("../"):

            if not self._executable_path:
                self._deadline_plugin.FailRender("Could not find executable path to resolve relative path.")

            # Find executable root
            import re
            engine_dir = re.findall("([\s\S]*.Engine)", self._executable_path)
            if not engine_dir:
                self._deadline_plugin.FailRender("Could not find executable Engine directory.")

            executable_root = Path(engine_dir[0]).parent

            # Resolve editor relative paths
            found_paths = sorted(executable_root.rglob(str(uproject).replace("\\", "/").strip("../")))

            if not found_paths or len(found_paths) > 1:
                self._deadline_plugin.FailRender(
                    f"Found multiple uprojects relative to the root directory. There should only be one when a relative path is defined."
                )

            uproject = found_paths[0]

        # make sure the project exists
        if not FileUtils.FileExists(uproject.as_posix()):
            self._deadline_plugin.FailRender(f"Could not find `{uproject.as_posix()}`")

        # Set up the arguments to startup unreal.
        job_command_args = [
            '"{u_project}"'.format(u_project=uproject.as_posix()),
            self._deadline_plugin.GetPluginInfoEntryWithDefault("CommandLineArguments", ""),
            # Force "-log" otherwise there is no output from the executable
            "-log",
            "-unattended",
            "-stdout",
            "-allowstdoutlogverbosity",
        ]

        arguments = " ".join(job_command_args)
        self._deadline_plugin.LogInfo(f"Startup Arguments: `{arguments}`")

        return arguments

__init__(process_name, deadline_plugin, deadline_rpc_manager)

Constructor :param process_name: The name of this process :param deadline_plugin: An instance of the plugin :param deadline_rpc_manager: An instance of the rpc manager

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
def __init__(self, process_name, deadline_plugin, deadline_rpc_manager):
    """
    Constructor
    :param process_name: The name of this process
    :param deadline_plugin: An instance of the plugin
    :param deadline_rpc_manager: An instance of the rpc manager
    """
    if sys.version_info.major == 3:
        super().__init__()
    self.InitializeProcessCallback += self._initialize_process
    self.RenderExecutableCallback += self._render_executable
    self.RenderArgumentCallback += self._render_argument
    self._deadline_plugin = deadline_plugin
    self._deadline_rpc_manager = deadline_rpc_manager
    self._temp_rpc_client = None
    self._name = process_name
    self._executable_path = None

    # Elapsed time to check for connection
    self._process_wait_time = int(self._deadline_plugin.GetConfigEntryWithDefault("RPCWaitTime", "300"))

clean_up()

Called when the plugin cleanup is called

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
345
346
347
348
349
350
351
352
353
354
355
356
357
def clean_up(self):
    """
    Called when the plugin cleanup is called
    """
    self._deadline_plugin.LogInfo("Executing managed process cleanup.")
    # Clean up stdout handler callbacks.
    for stdoutHandler in self.StdoutHandlers:
        del stdoutHandler.HandleCallback

    del self.InitializeProcessCallback
    del self.RenderExecutableCallback
    del self.RenderArgumentCallback
    self._deadline_plugin.LogInfo("Managed Process Cleanup Finished.")

render_task()

Render a task

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
def render_task(self):
    """
    Render a task
    """

    # Fail the render is we do not have a manager running
    if not self._deadline_rpc_manager:
        self._deadline_plugin.FailRender("No rpc manager was running!")

    # Start a timer to monitor the process time
    start_time = time.time()

    # Get temp client connection
    if not self._temp_rpc_client:
        self._temp_rpc_client = self._deadline_rpc_manager.get_temporary_client_proxy()


    print("Is server and client connected?", self._temp_rpc_client.is_connected())

    # Make sure we have a manager running, and we can establish a connection
    if not self._temp_rpc_client.is_connected():
        # Wait for a connection. This polls the server thread till an
        # unreal process client has connected. It is very important that
        # a connection is established by the client to allow this process
        # to execute.
        while round(time.time() - start_time) <= self._process_wait_time:
            try:
                # keep checking to see if a client has connected
                if self._temp_rpc_client.is_connected():
                    self._deadline_plugin.LogInfo(
                        "Client connection established!!"
                    )
                    break
            except Exception:
                pass

            self._deadline_plugin.LogInfo("Waiting on client connection..")
            self._deadline_plugin.FlushMonitoredManagedProcessStdout(
                self._name
            )
            time.sleep(2)
        else:

            # Fail the render after waiting too long
            self._deadline_plugin.FailRender(
                "A connection was not established with an unreal process"
            )

    # if we are connected, wait till the process task is marked as
    # complete.
    while not self._temp_rpc_client.is_task_complete(
        self._deadline_plugin.GetCurrentTaskId()
    ):
        # Keep flushing stdout
        self._deadline_plugin.FlushMonitoredManagedProcessStdout(self._name)

    # Flush one last time
    self._deadline_plugin.FlushMonitoredManagedProcessStdout(self._name)

UnrealEnginePlugin

Bases: DeadlinePlugin

Deadline plugin to execute an Unreal Engine job. NB: This plugin makes no assumptions about what the render job is but has a few expectations. This plugin runs as a server in the deadline process and exposes a few Deadline functionalities over XML RPC. The managed process used by this plugin waits for a client to connect and continuously polls the RPC server till a task has been marked complete before exiting the process. This behavior however has a drawback. If for some reason your process does not mark a task complete after working on a command, the plugin will run the current task indefinitely until specified to end by the repository settings or manually.

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
class UnrealEnginePlugin(DeadlinePlugin):
    """
    Deadline plugin to execute an Unreal Engine job.
    NB: This plugin makes no assumptions about what the render job is but has a
    few expectations. This plugin runs as a server in the deadline process
    and exposes a few Deadline functionalities over XML RPC. The managed process
    used by this plugin waits for a client to connect and continuously polls the
    RPC server till a task has been marked complete before exiting the
    process. This behavior however has a drawback. If for some reason your
    process does not mark a task complete after working on a command,
    the plugin will run the current task indefinitely until specified to
    end by the repository settings or manually.
    """

    def __init__(self):
        """
        Constructor
        """
        if sys.version_info.major == 3:
            super().__init__()
        self.InitializeProcessCallback += self._on_initialize_process
        self.StartJobCallback += self._on_start_job
        self.RenderTasksCallback += self._on_render_tasks
        self.EndJobCallback += self._on_end_job
        self.MonitoredManagedProcessExitCallback += self._on_process_exit

        # Set the name of the managed process to the current deadline process ID
        self._unreal_process_name = f"UnrealEngine_{os.getpid()}"
        self.unreal_managed_process = None

        # Keep track of the RPC manager
        self._deadline_rpc_manager = None

        # Keep track of when Job Ended has been called
        self._job_ended = False

        # set the plugin to commandline mode by default. This will launch the
        # editor and wait for the process to exit. There is no communication
        # with the deadline process.
        self._commandline_mode = True

    def clean_up(self):
        """
        Plugin cleanup
        """
        del self.InitializeProcessCallback
        del self.StartJobCallback
        del self.RenderTasksCallback
        del self.EndJobCallback

        if self.unreal_managed_process:
            self.unreal_managed_process.clean_up()
            del self.unreal_managed_process

        del self.MonitoredManagedProcessExitCallback

    def _on_initialize_process(self):
        """
        Initialize the plugin
        """
        self.LogInfo("Initializing job plugin")
        self.SingleFramesOnly = False
        self.StdoutHandling = True
        self.PluginType = PluginType.Advanced
        self._commandline_mode = StringUtils.ParseBoolean(
            self.GetPluginInfoEntryWithDefault("CommandLineMode", "true")
        )

        if self._commandline_mode:
            self.AddStdoutHandlerCallback(
                ".*Progress: (\\d+)%.*"
            ).HandleCallback += self._handle_progress
            self.AddStdoutHandlerCallback(
                ".*"
            ).HandleCallback += self._handle_stdout

        self.LogInfo("Initialization complete!")

    def _on_start_job(self):
        """
        This is executed when the plugin picks up a job
        """

        # Skip if we are in commandline mode
        if self._commandline_mode:
            return

        self.LogInfo("Executing Start Job")

        # Get and set up the RPC manager for the plugin
        self._deadline_rpc_manager = self._setup_rpc_manager()

        # Get a managed process
        self.unreal_managed_process = UnrealEngineManagedProcess(
            self._unreal_process_name, self, self._deadline_rpc_manager
        )
        self.LogInfo("Done executing Start Job")

    def _setup_rpc_manager(self):
        """
        Get an RPC manager for the plugin.
        """
        self.LogInfo("Setting up RPC Manager")
        # Setting the port to `0` will get a random available port for the
        # processes to connect on. This will help avoid TIME_WAIT
        # issues with the client if the job has to be re-queued
        port = 0

        # Get an instance of the deadline rpc manager class. This class will
        # store an instance of this plugin in the python globals. This should
        # allow threads in the process to get an instance of the plugin without
        # passing the data down through the thread instance
        _deadline_rpc_manager = DeadlineRPCServerManager(self, port)

        # We would like to run the server in a thread to not block deadline's
        # process. Get the Deadline RPC thread class. Set the class that is
        # going to be registered on the server on the thread class
        DeadlineRPCServerThread.deadline_job_manager = BaseDeadlineRPCJobManager

        # Set the threading class on the deadline manager
        _deadline_rpc_manager.threaded_server_class = DeadlineRPCServerThread

        return _deadline_rpc_manager

    def _on_render_tasks(self):
        """
        Execute the render task
        """
        # This starts a self-managed process that terminates based on the exit
        # code of the process. 0 means success
        if self._commandline_mode:
            startup_dir = self._get_startup_directory()

            self.unreal_managed_process = UnrealEngineCmdManagedProcess(
                self, self._unreal_process_name, startup_dir=startup_dir
            )

            # Auto execute the managed process
            self.RunManagedProcess(self.unreal_managed_process)
            exit_code = self.unreal_managed_process.ExitCode  # type: ignore

            self.LogInfo(f"Process returned: {exit_code}")

            if exit_code != 0:
                self.FailRender(
                    f"Process returned non-zero exit code '{exit_code}'"
                )

        else:
            # Flush stdout. This is useful after executing the first task
            self.FlushMonitoredManagedProcessStdout(self._unreal_process_name)

            # Start next tasks
            self.LogWarning(f"Starting Task {self.GetCurrentTaskId()}")

            # Account for any re-queued jobs. Deadline will immediately execute
            # render tasks if a job has been re-queued on the same process. If
            # that happens get a new instance of the rpc manager
            if not self._deadline_rpc_manager or self._job_ended:
                self._deadline_rpc_manager = self._setup_rpc_manager()

            if not self._deadline_rpc_manager.is_started:

                # Start the manager
                self._deadline_rpc_manager.start(threaded=True)

                # Get the socket the server is using and expose it to the
                # process
                server = self._deadline_rpc_manager.get_server()

                _, server_port = server.socket.getsockname()

                self.LogWarning(
                    f"Starting Deadline RPC Manager on port `{server_port}`"
                )

                # Get the port the server socket is going to use and
                # allow other systems to get the port to the rpc server from the
                # process environment variables
                self.SetProcessEnvironmentVariable(
                    "DEADLINE_RPC_PORT", str(server_port)
                )

            # Fail if we don't have an instance to a managed process.
            # This should typically return true
            if not self.unreal_managed_process:
                self.FailRender("There is no unreal process Running")

            if not self.MonitoredManagedProcessIsRunning(self._unreal_process_name):
                # Start the monitored Process
                self.StartMonitoredManagedProcess(
                    self._unreal_process_name,
                    self.unreal_managed_process
                )

                self.VerifyMonitoredManagedProcess(self._unreal_process_name)

            # Execute the render task
            self.unreal_managed_process.render_task()

            self.LogWarning(f"Finished Task {self.GetCurrentTaskId()}")
            self.FlushMonitoredManagedProcessStdout(self._unreal_process_name)

    def _on_end_job(self):
        """
        Called when the job ends
        """
        if self._commandline_mode:
            return

        self.FlushMonitoredManagedProcessStdout(self._unreal_process_name)
        self.LogWarning("EndJob called")
        self.ShutdownMonitoredManagedProcess(self._unreal_process_name)

        # Gracefully shutdown the RPC manager. This will also shut down any
        # threads spun up by the manager
        if self._deadline_rpc_manager:
            self._deadline_rpc_manager.shutdown()

        # Mark the job as ended. This also helps us to know when a job has
        # been re-queued, so we can get a new instance of the RPC manager,
        # as Deadline calls End Job when an error occurs
        self._job_ended = True

    def _on_process_exit(self):
        # If the process ends unexpectedly, make sure we shut down the manager
        # gracefully
        if self._commandline_mode:
            return

        if self._deadline_rpc_manager:
            self._deadline_rpc_manager.shutdown()

    def _handle_stdout(self):
        """
        Handle stdout
        """
        self._deadline_plugin.LogInfo(self.GetRegexMatch(0))

    def _handle_progress(self):
        """
        Handles any progress reports
        :return:
        """
        progress = float(self.GetRegexMatch(1))
        self.SetProgress(progress)

    def _get_startup_directory(self):
        """
        Get startup directory
        """
        startup_dir = self.GetPluginInfoEntryWithDefault(
            "StartupDirectory", ""
        ).strip()
        # Get the project root path
        project_root = self.GetProcessEnvironmentVariable("ProjectRoot")

        if startup_dir:
            if project_root:
                startup_dir = startup_dir.format(ProjectRoot=project_root)

            self.LogInfo("Startup Directory: {dir}".format(dir=startup_dir))
            return startup_dir.replace("\\", "/")

__init__()

Constructor

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def __init__(self):
    """
    Constructor
    """
    if sys.version_info.major == 3:
        super().__init__()
    self.InitializeProcessCallback += self._on_initialize_process
    self.StartJobCallback += self._on_start_job
    self.RenderTasksCallback += self._on_render_tasks
    self.EndJobCallback += self._on_end_job
    self.MonitoredManagedProcessExitCallback += self._on_process_exit

    # Set the name of the managed process to the current deadline process ID
    self._unreal_process_name = f"UnrealEngine_{os.getpid()}"
    self.unreal_managed_process = None

    # Keep track of the RPC manager
    self._deadline_rpc_manager = None

    # Keep track of when Job Ended has been called
    self._job_ended = False

    # set the plugin to commandline mode by default. This will launch the
    # editor and wait for the process to exit. There is no communication
    # with the deadline process.
    self._commandline_mode = True

clean_up()

Plugin cleanup

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def clean_up(self):
    """
    Plugin cleanup
    """
    del self.InitializeProcessCallback
    del self.StartJobCallback
    del self.RenderTasksCallback
    del self.EndJobCallback

    if self.unreal_managed_process:
        self.unreal_managed_process.clean_up()
        del self.unreal_managed_process

    del self.MonitoredManagedProcessExitCallback

CleanupDeadlinePlugin(deadline_plugin)

Deadline call this function to run any cleanup code :param deadline_plugin: An instance of the deadline plugin

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
28
29
30
31
32
33
def CleanupDeadlinePlugin(deadline_plugin):
    """
    Deadline call this function to run any cleanup code
    :param deadline_plugin: An instance of the deadline plugin
    """
    deadline_plugin.clean_up()

GetDeadlinePlugin()

Deadline calls this function to get am instance of our class

Source code in client/ayon_deadline/repository/custom/plugins/UnrealEngine5/UnrealEngine5.py
21
22
23
24
25
def GetDeadlinePlugin():
    """
    Deadline calls this function to get am instance of our class
    """
    return UnrealEnginePlugin()