Repository: ganga-devs/ganga (language: python)

Issue:
Interactive backend should save a log file
Would be helpful to have a `stdout` saved in the gangadir for Interactive jobs.


Discussion / hints:
hey @mesmith75 I would very much like to work on it. Could you help get started with it as rn I am pretty new to it?
Great! The script that runs when you submit an interactive job (`j.backend=Interactive()`) is here: https://github.com/ganga-devs/ganga/blob/develop/ganga/GangaCore/Lib/Interactive/InteractiveScriptTemplate.py.template

The interactive job currently only prints output to the screen. It would be useful to have the output also piped to a log file which then gets copied to the job's outputdir, as happens for the `Local` backend.
Hey @mesmith75  I am not able to use ganag. I installed it on linux virtual machine but command is not being detected. 
how did you install it? did you use pip?
yes , since I have windows so I first setup virtual ubuntu machine. Then I installed python. After that I installed ganga using pip install ganga. It did get installed but i tried using it by typing ganga but it isn't recognising it.
I did the following from a new ubuntu container
```shell
apt update && apt upgrade
apt install python3 python3-pip
pip install ganga
```
this was enough to install ganga and have the "binary" on the path
I tried it but am still not able to use it. Same error is shown again
Maybe it's because of virtual machine.
hey now I am able to run ganga.

Fix patch:
diff --git a/ganga/GangaCore/Lib/Interactive/InteractiveScriptTemplate.py.template b/ganga/GangaCore/Lib/Interactive/InteractiveScriptTemplate.py.template
index 7cbfceade5..09b488587d 100644
--- a/ganga/GangaCore/Lib/Interactive/InteractiveScriptTemplate.py.template
+++ b/ganga/GangaCore/Lib/Interactive/InteractiveScriptTemplate.py.template
@@ -5,6 +5,7 @@ import glob
 import mimetypes
 import os
 import shutil
+import subprocess
 import sys
 import time
 
@@ -13,36 +14,38 @@ import time
 
 ###WNSANDBOX_SOURCE###
 
+sys.path.insert(0, '###GANGA_PYTHONPATH###')
 
-sys.path.insert( 0, '###GANGA_PYTHONPATH###' )
+statfileName = os.path.join('###OUTPUTDIR###', '__jobstatus__')
+logfile = os.path.join('###OUTPUTDIR###', 'stdout')
+errfile = os.path.join('###OUTPUTDIR###', 'stderr')
 
-statfileName = os.path.join( '###OUTPUTDIR###', '__jobstatus__' )
 try:
-   statfile = open( statfileName, 'w' )
+   statfile = open(statfileName, 'w')
 except IOError as x:
    print('ERROR: Unable to write status file: %s' % statfileName)
-   print('ERROR: ',x)
+   print('ERROR: ', x)
    raise
 
-idfileName = os.path.join( '###OUTPUTDIR###', '__id__' )
+idfileName = os.path.join('###OUTPUTDIR###', '__id__')
 try:
-   idfile = open( idfileName, 'w' )
+   idfile = open(idfileName, 'w')
 except IOError as x:
    print('ERROR: Unable to write id file: %s' % idfileName)
-   print('ERROR: ',x)
+   print('ERROR: ', x)
    raise
 finally:
    idfile.close()
 
-timeString = time.strftime( '%a %d %b %H:%M:%S %Y', time.gmtime( time.time() ) )
-statfile.write( 'START: ' + timeString + os.linesep )
+timeString = time.strftime('%a %d %b %H:%M:%S %Y', time.gmtime(time.time()))
+statfile.write('START: ' + timeString + os.linesep)
 
-if not os.path.exists( '###WORKDIR###'):
+if not os.path.exists('###WORKDIR###'):
     os.makedirs('###WORKDIR###')
-os.chdir( '###WORKDIR###' )
+os.chdir('###WORKDIR###')
 for inFile in ###IN_BOX###:
     if mimetypes.guess_type(inFile)[1] in ['gzip', 'bzip2']:
-        getPackedInputSandbox( inFile )
+        getPackedInputSandbox(inFile)
     else:
         shutil.copy(inFile, os.path.join(os.getcwd(), os.path.basename(inFile)))
 sys.path.insert(0, os.path.join(os.getcwd(), '_python'))
@@ -52,7 +55,7 @@ sys.path.insert(0, os.path.join(os.getcwd(), '_python'))
 ###WN_INPUTDATA###
 
 for key, value in ###JOBCONFIG_ENV###.items():
-   os.environ[ key ] = value
+   os.environ[key] = value
 
 pyCommandList = [
    'import os',
@@ -60,18 +63,41 @@ pyCommandList = [
    'idfile = open( idfileName, \'a\' )',
    'idfile.write( \'PID: \' + str( os.getppid() ) )',
    'idfile.flush()',
-   'idfile.close()' ]
-pyCommandString = ';'.join( pyCommandList )
+   'idfile.close()']
+pyCommandString = ';'.join(pyCommandList)
 
 commandStr = '\'' + str(r'###EXE_STRING### ###ARG_STRING###') + '\''
-#commandStr = commandStr.replace('"', '\'')
+# commandStr = commandStr.replace('"', '\'')
 
 commandList = [
    'python -c "%s' % pyCommandString,
-   'os.system(%s)"' % commandStr  ]
-commandString = ';'.join( commandList )
+   'os.system(%s)"' % commandStr]
+commandString = ';'.join(commandList)
 
-result = os.system( '%s' % commandString )
+try:
+    with open(logfile, 'wb') as stdout, open(errfile, 'wb') as stderr:
+        process = subprocess.Popen(commandString, shell=True,
+                                stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                                stdin=subprocess.PIPE)
+
+        while True:
+            output = process.stdout.readline()
+            if output == b'' and process.poll() is not None:
+                break
+            if output:
+                print(output.strip().decode('utf-8'))
+                stdout.write(output)
+            err = process.stderr.readline()
+            if err:
+                print(err.strip().decode('utf-8'), file=sys.stderr)
+                stderr.write(err)
+
+        result = process.poll()
+
+except IOError as x:
+    print('ERROR: Unable to write status file or log file.')
+    print('ERROR: ', x)
+    raise
 
 ###WN_POSTPROCESSING###
 for patternToZip in ###PATTERNS_TO_ZIP###:
@@ -80,10 +106,8 @@ for patternToZip in ###PATTERNS_TO_ZIP###:
 
 createOutputSandbox( ###OUTPUT_SANDBOX_PATTERNS###, None, '###OUTPUTDIR###' )
 
-statfile.write( 'EXITCODE: ' + str( result >> 8 ) + os.linesep )
-timeString = time.strftime( '%a %d %b %H:%M:%S %Y', time.gmtime( time.time() ) )
-statfile.write( 'STOP: ' + timeString + os.linesep )
+statfile.write('EXITCODE: ' + str(result >> 8) + os.linesep)
+timeString=time.strftime('%a %d %b %H:%M:%S %Y', time.gmtime(time.time()))
+statfile.write('STOP: ' + timeString + os.linesep)
 statfile.flush()
 statfile.close()
-
-


Test patch:
diff --git a/ganga/GangaCore/test/GPI/TestInteractive.py b/ganga/GangaCore/test/GPI/TestInteractive.py
new file mode 100644
index 0000000000..2074f8f726
--- /dev/null
+++ b/ganga/GangaCore/test/GPI/TestInteractive.py
@@ -0,0 +1,36 @@
+from GangaCore.testlib.GangaUnitTest import GangaUnitTest
+
+
+class TestInteractiveJobOutput(GangaUnitTest):
+    def testInteractiveJobOutput(self):
+        from GangaCore.GPI import Job, Executable, Interactive, LocalFile
+        from GangaTest.Framework.utils import sleep_until_completed
+        import os
+
+        app = Executable()
+        app.exe = 'echo'
+        app.args = ["Hello World", ""]
+        j = Job(backend=Interactive(), application=app,
+                outputfiles=[LocalFile('stdout'), LocalFile('stderr')])
+        j.submit()
+
+        self.assertTrue(sleep_until_completed(j, 60),
+                        'Timeout on registering Interactive job as completed')
+
+        stdout_path = os.path.join(j.outputdir, 'stdout')
+        stderr_path = os.path.join(j.outputdir, 'stderr')
+        self.assertTrue(os.path.exists(stdout_path), 'stdout file not created')
+        self.assertTrue(os.path.exists(stderr_path), 'stderr file not created')
+
+        expected_stdout_content = b'Hello World'
+        expected_stderr_content = b''
+
+        with open(stdout_path, 'rb') as stdout_file:
+            actual_stdout_content = stdout_file.read()
+            self.assertIn(expected_stdout_content, actual_stdout_content,
+                          'Incorrect content in stdout file')
+
+        with open(stderr_path, 'rb') as stderr_file:
+            actual_stderr_content = stderr_file.read()
+            self.assertIn(expected_stderr_content, actual_stderr_content,
+                          'Incorrect content in stderr file')