diff --git a/.github/workflows/testing-pipeline.yml b/.github/workflows/testing-pipeline.yml index 7d2b59af9..15967a4a9 100644 --- a/.github/workflows/testing-pipeline.yml +++ b/.github/workflows/testing-pipeline.yml @@ -7,41 +7,20 @@ on: branches: [ master ] jobs: - test_python_2019: - name: Run Python Unit Tests (CY2019) - runs-on: ubuntu-latest - container: aswf/ci-opencue:2019 - steps: - - uses: actions/checkout@v3 - - name: Run Python Tests - run: ci/run_python_tests.sh - - test_cuebot_2019: - name: Build Cuebot and Run Unit Tests (CY2019) - runs-on: ubuntu-latest - container: - image: aswf/ci-opencue:2019 - steps: - - uses: actions/checkout@v3 - - name: Build with Gradle - run: | - chown -R aswfuser:aswfgroup . - su -c "cd cuebot && ./gradlew build --stacktrace --info" aswfuser - - test_python_2020: - name: Run Python Unit Tests (CY2020) + test_python_2022: + name: Run Python Unit Tests (CY2022) runs-on: ubuntu-latest - container: aswf/ci-opencue:2020 + container: aswf/ci-opencue:2022 steps: - uses: actions/checkout@v3 - name: Run Python Tests - run: ci/run_python_tests.sh + run: ci/run_python_tests.sh --no-gui - test_cuebot_2020: - name: Build Cuebot and Run Unit Tests (CY2020) + test_cuebot_2022: + name: Build Cuebot and Run Unit Tests (CY2022) runs-on: ubuntu-latest container: - image: aswf/ci-opencue:2020 + image: aswf/ci-opencue:2022 steps: - uses: actions/checkout@v3 - name: Build with Gradle @@ -49,20 +28,20 @@ jobs: chown -R aswfuser:aswfgroup . su -c "cd cuebot && ./gradlew build --stacktrace --info" aswfuser - test_python_2021: - name: Run Python Unit Tests (CY2021) + test_python_2023: + name: Run Python Unit Tests (CY2023) runs-on: ubuntu-latest - container: aswf/ci-opencue:2021 + container: aswf/ci-opencue:2023 steps: - uses: actions/checkout@v3 - name: Run Python Tests run: ci/run_python_tests.sh - test_cuebot_2021: - name: Build Cuebot and Run Unit Tests (CY2021) + test_cuebot_2023: + name: Build Cuebot and Run Unit Tests (CY2023) runs-on: ubuntu-latest container: - image: aswf/ci-opencue:2021 + image: aswf/ci-opencue:2023 steps: - uses: actions/checkout@v3 - name: Build with Gradle @@ -70,27 +49,15 @@ jobs: chown -R aswfuser:aswfgroup . su -c "cd cuebot && ./gradlew build --stacktrace --info" aswfuser - test_python_2022: - name: Run Python Unit Tests (CY2022) + test_python2: + name: Run Python Unit Tests using Python2 runs-on: ubuntu-latest - container: aswf/ci-opencue:2022 + container: aswf/ci-opencue:2019 steps: - uses: actions/checkout@v3 - name: Run Python Tests run: ci/run_python_tests.sh - test_cuebot_2022: - name: Build Cuebot and Run Unit Tests (CY2022) - runs-on: ubuntu-latest - container: - image: aswf/ci-opencue:2022 - steps: - - uses: actions/checkout@v3 - - name: Build with Gradle - run: | - chown -R aswfuser:aswfgroup . - su -c "cd cuebot && ./gradlew build --stacktrace --info" aswfuser - test_pyside6: name: Run CueGUI Tests using PySide6 runs-on: ubuntu-latest @@ -113,7 +80,7 @@ jobs: name: Test Documentation Build runs-on: ubuntu-latest container: - image: aswf/ci-opencue:2020 + image: aswf/ci-opencue:2023 steps: - uses: actions/checkout@v3 - name: Run Sphinx build diff --git a/ci/run_python_tests.sh b/ci/run_python_tests.sh index c6a51a015..5f1bfe294 100755 --- a/ci/run_python_tests.sh +++ b/ci/run_python_tests.sh @@ -1,7 +1,13 @@ #!/bin/bash +# Script for running OpenCue unit tests with PySide2. +# +# This script is written to be run within the OpenCue GitHub Actions environment. +# See `.github/workflows/testing-pipeline.yml`. + set -e +args=("$@") python_version=$(python -V 2>&1) echo "Will run tests using ${python_version}" @@ -23,6 +29,6 @@ PYTHONPATH=pycue:pyoutline python cuesubmit/setup.py test python rqd/setup.py test # Xvfb no longer supports Python 2. -if [[ "$python_version" =~ "Python 3" ]]; then +if [[ "$python_version" =~ "Python 3" && ${args[0]} != "--no-gui" ]]; then ci/run_gui_test.sh fi diff --git a/ci/run_python_tests_pyside6.sh b/ci/run_python_tests_pyside6.sh new file mode 100755 index 000000000..384841cfe --- /dev/null +++ b/ci/run_python_tests_pyside6.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Script for running OpenCue unit tests with PySide6. +# +# This script is written to be run within the OpenCue GitHub Actions environment. +# See `.github/workflows/testing-pipeline.yml`. + +set -e + +python_version=$(python -V 2>&1) +echo "Will run tests using ${python_version}" + +# NOTE: To run this in an almalinux environment, install these packages: +# yum -y install \ +# dbus-libs \ +# fontconfig \ +# gcc \ +# libxkbcommon-x11 \ +# mesa-libEGL-devel \ +# python-devel \ +# which \ +# xcb-util-keysyms \ +# xcb-util-image \ +# xcb-util-renderutil \ +# xcb-util-wm \ +# Xvfb + +# Install Python requirements. +python3 -m pip install --user -r requirements.txt -r requirements_gui.txt +# Replace PySide2 with PySide6. +python3 -m pip uninstall -y PySide2 +python3 -m pip install --user PySide6==6.3.2 + +# Protos need to have their Python code generated in order for tests to pass. +python -m grpc_tools.protoc -I=proto/ --python_out=pycue/opencue/compiled_proto --grpc_python_out=pycue/opencue/compiled_proto proto/*.proto +python -m grpc_tools.protoc -I=proto/ --python_out=rqd/rqd/compiled_proto --grpc_python_out=rqd/rqd/compiled_proto proto/*.proto + +# Fix compiled proto code for Python 3. +2to3 -wn -f import pycue/opencue/compiled_proto/*_pb2*.py +2to3 -wn -f import rqd/rqd/compiled_proto/*_pb2*.py + +python pycue/setup.py test +PYTHONPATH=pycue python pyoutline/setup.py test +PYTHONPATH=pycue python cueadmin/setup.py test +PYTHONPATH=pycue:pyoutline python cuesubmit/setup.py test +python rqd/setup.py test + +ci/run_gui_test.sh diff --git a/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/JobDaoJdbc.java b/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/JobDaoJdbc.java index d32a5a259..a5f595f4e 100644 --- a/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/JobDaoJdbc.java +++ b/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/JobDaoJdbc.java @@ -574,7 +574,7 @@ public void activateJob(JobInterface job, JobState jobState) { jobTotals[0] + jobTotals[1], layers.size(), job.getJobId()); getJdbcTemplate().update( - "UPDATE show SET int_frame_insert_count=int_frame_insert_count+?, int_job_insert_count=int_job_insert_count+1 WHERE pk_show=?", + "UPDATE show_stats SET int_frame_insert_count=int_frame_insert_count+?, int_job_insert_count=int_job_insert_count+1 WHERE pk_show=?", jobTotals[0] + jobTotals[1], job.getShowId()); updateState(job, jobState); diff --git a/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/ShowDaoJdbc.java b/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/ShowDaoJdbc.java index 8361a6c5f..86e126559 100644 --- a/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/ShowDaoJdbc.java +++ b/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/ShowDaoJdbc.java @@ -138,9 +138,15 @@ public ShowEntity getShowDetail(HostInterface host) { private static final String INSERT_SHOW = "INSERT INTO show (pk_show,str_name) VALUES (?,?)"; + private static final String INSERT_SHOW_STATS = + "INSERT INTO show_stats " + + "(pk_show, int_frame_insert_count, int_job_insert_count, int_frame_success_count, int_frame_fail_count) " + + "VALUES (?, 0, 0, 0, 0)"; + public void insertShow(ShowEntity show) { show.id = SqlUtil.genKeyRandom(); getJdbcTemplate().update(INSERT_SHOW, show.id, show.name); + getJdbcTemplate().update(INSERT_SHOW_STATS, show.id); } private static final String SHOW_EXISTS = @@ -169,6 +175,8 @@ public void delete(ShowInterface s) { s.getShowId()); getJdbcTemplate().update("DELETE FROM show_alias WHERE pk_show=?", s.getShowId()); + getJdbcTemplate().update("DELETE FROM show_stats WHERE pk_show=?", + s.getShowId()); getJdbcTemplate().update("DELETE FROM show WHERE pk_show=?", s.getShowId()); } @@ -262,7 +270,7 @@ public void updateFrameCounters(ShowInterface s, int exitStatus) { col = "int_frame_fail_count = int_frame_fail_count + 1"; } getJdbcTemplate().update( - "UPDATE show SET " + col + " WHERE pk_show=?", s.getShowId()); + "UPDATE show_stats SET " + col + " WHERE pk_show=?", s.getShowId()); } } diff --git a/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/WhiteboardDaoJdbc.java b/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/WhiteboardDaoJdbc.java index 38566470a..fe3217f96 100644 --- a/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/WhiteboardDaoJdbc.java +++ b/cuebot/src/main/java/com/imageworks/spcue/dao/postgres/WhiteboardDaoJdbc.java @@ -2060,7 +2060,21 @@ public Show mapRow(ResultSet rs, int rowNum) throws SQLException { private static final String GET_SHOW = "SELECT " + - "show.*," + + "show.pk_show," + + "show.str_name," + + "show.b_paused," + + "show.int_default_min_cores," + + "show.int_default_max_cores," + + "show.int_default_min_gpus," + + "show.int_default_max_gpus," + + "show.b_booking_enabled," + + "show.b_dispatch_enabled," + + "show.b_active," + + "show.str_comment_email," + + "show_stats.int_frame_insert_count," + + "show_stats.int_job_insert_count," + + "show_stats.int_frame_success_count," + + "show_stats.int_frame_fail_count," + "COALESCE(vs_show_stat.int_pending_count,0) AS int_pending_count," + "COALESCE(vs_show_stat.int_running_count,0) AS int_running_count," + "COALESCE(vs_show_stat.int_dead_count,0) AS int_dead_count," + @@ -2069,6 +2083,7 @@ public Show mapRow(ResultSet rs, int rowNum) throws SQLException { "COALESCE(vs_show_stat.int_job_count,0) AS int_job_count " + "FROM " + "show " + + "JOIN show_stats ON (show.pk_show = show_stats.pk_show) " + "LEFT JOIN vs_show_stat ON (vs_show_stat.pk_show = show.pk_show) " + "LEFT JOIN vs_show_resource ON (vs_show_resource.pk_show=show.pk_show) " + "WHERE " + diff --git a/cuebot/src/main/resources/conf/ddl/postgres/migrations/V16__AddShowStats.sql b/cuebot/src/main/resources/conf/ddl/postgres/migrations/V16__AddShowStats.sql new file mode 100644 index 000000000..f97c14f7c --- /dev/null +++ b/cuebot/src/main/resources/conf/ddl/postgres/migrations/V16__AddShowStats.sql @@ -0,0 +1,33 @@ +CREATE TABLE show_stats ( + pk_show VARCHAR(36) NOT NULL, + int_frame_insert_count BIGINT DEFAULT 0 NOT NULL, + int_job_insert_count BIGINT DEFAULT 0 NOT NULL, + int_frame_success_count BIGINT DEFAULT 0 NOT NULL, + int_frame_fail_count BIGINT DEFAULT 0 NOT NULL +); + +INSERT INTO show_stats ( + pk_show, + int_frame_insert_count, + int_job_insert_count, + int_frame_success_count, + int_frame_fail_count +) SELECT + pk_show, + int_frame_insert_count, + int_job_insert_count, + int_frame_success_count, + int_frame_fail_count + FROM show; + +CREATE UNIQUE INDEX c_show_stats_pk ON show_stats (pk_show); +ALTER TABLE show_stats ADD CONSTRAINT c_show_stats_pk PRIMARY KEY + USING INDEX c_show_stats_pk; + + +-- Destructive changes. Please test changes above prior to executing this. +ALTER TABLE show + DROP COLUMN int_frame_insert_count, + DROP COLUMN int_job_insert_count, + DROP COLUMN int_frame_success_count, + DROP COLUMN int_frame_fail_count; diff --git a/cuebot/src/main/resources/conf/ddl/postgres/seed_data.sql b/cuebot/src/main/resources/conf/ddl/postgres/seed_data.sql index ebebaa75d..7b189174c 100644 --- a/cuebot/src/main/resources/conf/ddl/postgres/seed_data.sql +++ b/cuebot/src/main/resources/conf/ddl/postgres/seed_data.sql @@ -1,4 +1,6 @@ -Insert into SHOW (PK_SHOW,STR_NAME,INT_DEFAULT_MAX_CORES,INT_DEFAULT_MIN_CORES,INT_FRAME_INSERT_COUNT,INT_JOB_INSERT_COUNT,INT_FRAME_SUCCESS_COUNT,INT_FRAME_FAIL_COUNT,B_BOOKING_ENABLED,B_DISPATCH_ENABLED,B_ACTIVE) values ('00000000-0000-0000-0000-000000000000', 'testing', 200000, 100, 0, 0, 0, 0, true, true, true); +Insert into SHOW (PK_SHOW,STR_NAME,INT_DEFAULT_MAX_CORES,INT_DEFAULT_MIN_CORES,B_BOOKING_ENABLED,B_DISPATCH_ENABLED,B_ACTIVE) values ('00000000-0000-0000-0000-000000000000', 'testing', 200000, 100, true, true, true); + +Insert into SHOW_STATS (PK_SHOW,INT_FRAME_INSERT_COUNT,INT_JOB_INSERT_COUNT,INT_FRAME_SUCCESS_COUNT,INT_FRAME_FAIL_COUNT) values ('00000000-0000-0000-0000-000000000000',0,0,0,0); Insert into SHOW_ALIAS (PK_SHOW_ALIAS,PK_SHOW,STR_NAME) values ('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000000', 'test'); diff --git a/cuebot/src/test/java/com/imageworks/spcue/test/dao/postgres/ShowDaoTests.java b/cuebot/src/test/java/com/imageworks/spcue/test/dao/postgres/ShowDaoTests.java index d430ab3b0..e44393ab6 100644 --- a/cuebot/src/test/java/com/imageworks/spcue/test/dao/postgres/ShowDaoTests.java +++ b/cuebot/src/test/java/com/imageworks/spcue/test/dao/postgres/ShowDaoTests.java @@ -219,20 +219,20 @@ public void testUpdateActive() { public void testUpdateFrameCounters() { ShowEntity show = showDao.findShowDetail(SHOW_NAME); int frameSuccess = jdbcTemplate.queryForObject( - "SELECT int_frame_success_count FROM show WHERE pk_show=?", + "SELECT int_frame_success_count FROM show_stats WHERE pk_show=?", Integer.class, show.id); showDao.updateFrameCounters(show, 0); int frameSucces2 = jdbcTemplate.queryForObject( - "SELECT int_frame_success_count FROM show WHERE pk_show=?", + "SELECT int_frame_success_count FROM show_stats WHERE pk_show=?", Integer.class, show.id); assertEquals(frameSuccess + 1,frameSucces2); int frameFail= jdbcTemplate.queryForObject( - "SELECT int_frame_fail_count FROM show WHERE pk_show=?", + "SELECT int_frame_fail_count FROM show_stats WHERE pk_show=?", Integer.class, show.id); showDao.updateFrameCounters(show, 1); int frameFail2 = jdbcTemplate.queryForObject( - "SELECT int_frame_fail_count FROM show WHERE pk_show=?", + "SELECT int_frame_fail_count FROM show_stats WHERE pk_show=?", Integer.class, show.id); assertEquals(frameFail+ 1,frameFail2); } diff --git a/cuebot/src/test/resources/conf/ddl/postgres/test_data.sql b/cuebot/src/test/resources/conf/ddl/postgres/test_data.sql index ae7b77354..4f9c2a0a0 100644 --- a/cuebot/src/test/resources/conf/ddl/postgres/test_data.sql +++ b/cuebot/src/test/resources/conf/ddl/postgres/test_data.sql @@ -1,6 +1,10 @@ -Insert into SHOW (PK_SHOW,STR_NAME,INT_DEFAULT_MAX_CORES,INT_DEFAULT_MIN_CORES,INT_FRAME_INSERT_COUNT,INT_JOB_INSERT_COUNT,INT_FRAME_SUCCESS_COUNT,INT_FRAME_FAIL_COUNT,B_BOOKING_ENABLED,B_DISPATCH_ENABLED,B_ACTIVE) values ('00000000-0000-0000-0000-000000000000','pipe',20000,100,0,0,0,0,true,true,true) +Insert into SHOW (PK_SHOW,STR_NAME,INT_DEFAULT_MAX_CORES,INT_DEFAULT_MIN_CORES,B_BOOKING_ENABLED,B_DISPATCH_ENABLED,B_ACTIVE) values ('00000000-0000-0000-0000-000000000000','pipe',20000,100,true,true,true) -Insert into SHOW (PK_SHOW,STR_NAME,INT_DEFAULT_MAX_CORES,INT_DEFAULT_MIN_CORES,INT_FRAME_INSERT_COUNT,INT_JOB_INSERT_COUNT,INT_FRAME_SUCCESS_COUNT,INT_FRAME_FAIL_COUNT,B_BOOKING_ENABLED,B_DISPATCH_ENABLED,B_ACTIVE) values ('00000000-0000-0000-0000-000000000001','edu',20000,100,0,0,0,0,true,true,true) +Insert into SHOW (PK_SHOW,STR_NAME,INT_DEFAULT_MAX_CORES,INT_DEFAULT_MIN_CORES,B_BOOKING_ENABLED,B_DISPATCH_ENABLED,B_ACTIVE) values ('00000000-0000-0000-0000-000000000001','edu',20000,100,true,true,true) + +Insert into SHOW_STATS (PK_SHOW,INT_FRAME_INSERT_COUNT,INT_JOB_INSERT_COUNT,INT_FRAME_SUCCESS_COUNT,INT_FRAME_FAIL_COUNT) values ('00000000-0000-0000-0000-000000000000',0,0,0,0) + +Insert into SHOW_STATS (PK_SHOW,INT_FRAME_INSERT_COUNT,INT_JOB_INSERT_COUNT,INT_FRAME_SUCCESS_COUNT,INT_FRAME_FAIL_COUNT) values ('00000000-0000-0000-0000-000000000001',0,0,0,0) Insert into SHOW_ALIAS (PK_SHOW_ALIAS,PK_SHOW,STR_NAME) values ('00000000-0000-0000-0000-000000000001','00000000-0000-0000-0000-000000000000','fx') diff --git a/cuegui/tests/Constants_tests.py b/cuegui/tests/Constants_tests.py index 9466dcece..3cfe3866f 100644 --- a/cuegui/tests/Constants_tests.py +++ b/cuegui/tests/Constants_tests.py @@ -25,7 +25,7 @@ import mock import pyfakefs.fake_filesystem_unittest -from PySide2 import QtGui +from qtpy import QtGui import opencue import cuegui.Constants @@ -40,6 +40,7 @@ ''' +# pylint: disable=import-outside-toplevel,redefined-outer-name,reimported class ConstantsTests(pyfakefs.fake_filesystem_unittest.TestCase): def setUp(self): self.setUpPyfakefs() @@ -53,6 +54,7 @@ def test__should_load_user_config_from_env_var(self): self.fs.create_file(config_file_path, contents=CONFIG_YAML) os.environ['CUEGUI_CONFIG_FILE'] = config_file_path + import cuegui.Constants result = importlib.reload(cuegui.Constants) self.assertEqual('98.707.68', result.VERSION) @@ -65,6 +67,7 @@ def test__should_load_user_config_from_user_profile(self): config_file_path = '/home/username/.config/opencue/cuegui.yaml' self.fs.create_file(config_file_path, contents=CONFIG_YAML) + import cuegui.Constants result = importlib.reload(cuegui.Constants) self.assertEqual('98.707.68', result.VERSION) @@ -73,6 +76,7 @@ def test__should_load_user_config_from_user_profile(self): @mock.patch('platform.system', new=mock.Mock(return_value='Linux')) def test__should_use_default_values(self): + import cuegui.Constants result = importlib.reload(cuegui.Constants) self.assertNotEqual('98.707.68', result.VERSION) @@ -161,6 +165,7 @@ def test__should_use_default_values(self): @mock.patch('platform.system', new=mock.Mock(return_value='Darwin')) def test__should_use_mac_editor(self): + import cuegui.Constants result = importlib.reload(cuegui.Constants) self.assertEqual('open -t', result.DEFAULT_EDITOR) diff --git a/requirements.txt b/requirements.txt index 4ca2a8b42..262f681f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,8 @@ packaging==20.9 pathlib==1.0.1;python_version<"3.4" protobuf==3.17.3;python_version<"3.0" psutil==5.6.7 -pyfakefs==3.6 +pyfakefs==3.6;python_version<"3.7" +pyfakefs==5.2.3;python_version>="3.7" pylint==2.6.0;python_version>="3.7" pynput==1.7.6 PyYAML==5.1 diff --git a/rqd/rqd/rqconstants.py b/rqd/rqd/rqconstants.py index ad82b5daf..fe793970e 100644 --- a/rqd/rqd/rqconstants.py +++ b/rqd/rqd/rqconstants.py @@ -73,6 +73,7 @@ RQD_BECOME_JOB_USER = True RQD_CREATE_USER_IF_NOT_EXISTS = True RQD_TAGS = '' +RQD_PREPEND_TIMESTAMP = False KILL_SIGNAL = 9 if platform.system() == 'Linux': @@ -197,6 +198,8 @@ if config.has_option(__section, "FILE_LOG_LEVEL"): level = config.get(__section, "FILE_LOG_LEVEL") FILE_LOG_LEVEL = logging.getLevelName(level) + if config.has_option(__section, "RQD_PREPEND_TIMESTAMP"): + RQD_PREPEND_TIMESTAMP = config.getboolean(__section, "RQD_PREPEND_TIMESTAMP") # pylint: disable=broad-except except Exception as e: logging.warning( diff --git a/rqd/rqd/rqcore.py b/rqd/rqd/rqcore.py index 224485f2b..842028732 100644 --- a/rqd/rqd/rqcore.py +++ b/rqd/rqd/rqcore.py @@ -315,14 +315,17 @@ def runLinux(self): else: tempCommand += [self._createCommandFile(runFrame.command)] - # Actual cwd is set by /shots/SHOW/home/perl/etc/qwrap.cuerun + if rqd.rqconstants.RQD_PREPEND_TIMESTAMP: + file_descriptor = subprocess.PIPE + else: + file_descriptor = self.rqlog # pylint: disable=subprocess-popen-preexec-fn frameInfo.forkedCommand = subprocess.Popen(tempCommand, env=self.frameEnv, cwd=self.rqCore.machine.getTempPath(), stdin=subprocess.PIPE, - stdout=self.rqlog, - stderr=self.rqlog, + stdout=file_descriptor, + stderr=file_descriptor, close_fds=True, preexec_fn=os.setsid) finally: @@ -335,6 +338,8 @@ def runLinux(self): self.rqCore.updateRss) self.rqCore.updateRssThread.start() + if rqd.rqconstants.RQD_PREPEND_TIMESTAMP: + pipe_to_file(frameInfo.forkedCommand.stdout, frameInfo.forkedCommand.stderr, self.rqlog) returncode = frameInfo.forkedCommand.wait() # Find exitStatus and exitSignal @@ -535,7 +540,7 @@ def run(self): else: raise RuntimeError(err) try: - self.rqlog = open(runFrame.log_dir_file, "w", 1) + self.rqlog = open(runFrame.log_dir_file, "w+", 1) self.waitForFile(runFrame.log_dir_file) # pylint: disable=broad-except except Exception as e: @@ -1161,3 +1166,98 @@ def sendStatusReport(self): def isWaitingForIdle(self): """Returns whether the host is waiting until idle to take some action.""" return self.__whenIdle + +def pipe_to_file(stdout, stderr, outfile): + """ + Prepend entries on stdout and stderr with a timestamp and write to outfile. + + The logic to poll stdout/stderr is inspired by the Popen.communicate implementation. + This feature is linux specific + """ + # Importing packages internally to avoid compatibility issues with Windows + + if stdout is None or stderr is None: + return + outfile.flush() + os.fsync(outfile) + + # pylint: disable=import-outside-toplevel + import select + import errno + # pylint: enable=import-outside-toplevel + + fd2file = {} + fd2output = {} + + poller = select.poll() + + def register_and_append(file_ojb, eventmask): + poller.register(file_ojb, eventmask) + fd2file[file_ojb.fileno()] = file_ojb + + def close_and_unregister_and_remove(fd, close=False): + poller.unregister(fd) + if close: + fd2file[fd].close() + fd2file.pop(fd) + + def print_and_flush_ln(fd, last_timestamp): + txt = ''.join(fd2output[fd]) + lines = txt.split('\n') + next_line_timestamp = None + + # Save the timestamp of the first break + if last_timestamp is None: + curr_line_timestamp = datetime.datetime.now().strftime("%H:%M:%S") + else: + curr_line_timestamp = last_timestamp + + # There are no line breaks + if len(lines) < 2: + return curr_line_timestamp + next_line_timestamp = datetime.datetime.now().strftime("%H:%M:%S") + + remainder = lines[-1] + for line in lines[0:-1]: + print("[%s] %s" % (curr_line_timestamp, line), file=outfile) + outfile.flush() + os.fsync(outfile) + fd2output[fd] = [remainder] + + if next_line_timestamp is None: + return curr_line_timestamp + return next_line_timestamp + + def translate_newlines(data): + data = data.decode("utf-8", "ignore") + return data.replace("\r\n", "\n").replace("\r", "\n") + + select_POLLIN_POLLPRI = select.POLLIN | select.POLLPRI + # stdout + register_and_append(stdout, select_POLLIN_POLLPRI) + fd2output[stdout.fileno()] = [] + + # stderr + register_and_append(stderr, select_POLLIN_POLLPRI) + fd2output[stderr.fileno()] = [] + + while fd2file: + try: + ready = poller.poll() + except select.error as e: + if e.args[0] == errno.EINTR: + continue + raise + + first_chunk_timestamp = None + for fd, mode in ready: + if mode & select_POLLIN_POLLPRI: + data = os.read(fd, 4096) + if not data: + close_and_unregister_and_remove(fd) + if not isinstance(data, str): + data = translate_newlines(data) + fd2output[fd].append(data) + first_chunk_timestamp = print_and_flush_ln(fd, first_chunk_timestamp) + else: + close_and_unregister_and_remove(fd) diff --git a/rqd/tests/rqcore_tests.py b/rqd/tests/rqcore_tests.py index aad307dac..256b96b2b 100644 --- a/rqd/tests/rqcore_tests.py +++ b/rqd/tests/rqcore_tests.py @@ -567,6 +567,7 @@ def setUp(self): @mock.patch('platform.system', new=mock.Mock(return_value='Linux')) @mock.patch('tempfile.gettempdir') + @mock.patch('rqd.rqcore.pipe_to_file', new=mock.MagicMock()) def test_runLinux(self, getTempDirMock, permsUser, timeMock, popenMock): # mkdirMock, openMock, # given currentTime = 1568070634.3 @@ -632,8 +633,6 @@ def test_runLinux(self, getTempDirMock, permsUser, timeMock, popenMock): # mkdir self.assertTrue(os.path.exists(logDir)) self.assertTrue(os.path.isfile(logFile)) _, kwargs = popenMock.call_args - self.assertEqual(logFile, kwargs['stdout'].name) - self.assertEqual(logFile, kwargs['stderr'].name) rqCore.network.reportRunningFrameCompletion.assert_called_with( rqd.compiled_proto.report_pb2.FrameCompleteReport( diff --git a/tsc/meetings/2023-04-12.md b/tsc/meetings/2023-04-12.md new file mode 100644 index 000000000..43d001d65 --- /dev/null +++ b/tsc/meetings/2023-04-12.md @@ -0,0 +1,38 @@ +# OpenCue TSC Meeting Notes 12 Apr 2023 + +Secretary: Brian Cipriano + +Agenda/Notes: + +* Host ping time until marked as DOWN + * https://github.com/AcademySoftwareFoundation/OpenCue/issues/1265 + * This does seem long, any reason why? + * Diego: seems unusual. Should be less than a minute for host to be marked DOWN. CueGUI should + update 15-20s + * Should we make this a config setting? + * SPI to check on their code for differences + * Might need to lower default value, this is a good candidate for config flag. +* RQD config file overhaul + * https://github.com/AcademySoftwareFoundation/OpenCue/pull/1270 + * Would be good to get SPI input on this. + * Let's review in more detail. No immediate concerns. SPI has some similar configuration already +* Rez setup email thread + * https://lists.aswf.io/g/opencue-dev/topic/97805737#571 + * Diego: might make a better tutorial doc than merging into master branch. We don't want to + confuse new users with multiple packaging options. + * Look into spk, an OSS project. + * pip packages will make this setup much simpler. +* Prepending timestamps to RQD child process output + * https://github.com/AcademySoftwareFoundation/OpenCue/pull/1286 + * Doesn't modify any existing output other than prepending timestamp to each line. + * Linux-specific, configurable. +* Python 2 support + * Not ready to drop support for 2 entirely, especially python libraries. + * GUI should be fine to go 3-only. + * If we're going to do it, document which tag contains the last 2 support + * A py2 branch might be helpful if anyone wants to backport, but might have issues with our + versioning tooling. +* Blender plugin update + * Basic plugin is loading, currently navigating issues with installing pyoutline into Blender + environment. Will start to send test jobs soon + * Will continue to update email thread. diff --git a/tsc/meetings/2023-06-07.md b/tsc/meetings/2023-06-07.md new file mode 100644 index 000000000..98df98a61 --- /dev/null +++ b/tsc/meetings/2023-06-07.md @@ -0,0 +1,44 @@ +# OpenCue TSC Meeting Notes 7 Jun 2023 + +Secretary: Brian Cipriano + +Agenda/Notes: + +* Host ping time until marked as DOWN + * https://github.com/AcademySoftwareFoundation/OpenCue/issues/1265 + * Any update here? + * Needs some further verification and response. +* Appending timestamps to logs + * https://github.com/AcademySoftwareFoundation/OpenCue/pull/1286 + * LGTM, needs merge from master, looking into test failures. +* Cuesubmit batch of PRs + * https://github.com/AcademySoftwareFoundation/OpenCue/pull/1278 + * https://github.com/AcademySoftwareFoundation/OpenCue/pull/1280 + * https://github.com/AcademySoftwareFoundation/OpenCue/pull/1281 + * https://github.com/AcademySoftwareFoundation/OpenCue/pull/1282 + * https://github.com/AcademySoftwareFoundation/OpenCue/pull/1283 + * https://github.com/AcademySoftwareFoundation/OpenCue/pull/1284 + * https://github.com/AcademySoftwareFoundation/OpenCue/pull/1285 + * Reviews needed. + * Be careful we're not making CueSubmit too specialized, keep it generally useful for new + users. + * Let's invite the author to a TSC meeting soon. + * Improvements are good, is there something else we can offer? What would be helpful for + larger-studio users? Or is the Python library good enough? + * Best to expand pyoutline examples / docs to help developers who have already tried + CueSubmit. + * Build on basic example used in integration test script. +* Blender plugin update + * Currently testing job submission, blocked on some submission code. + * Loading python deps (opencue, filesequence) + * Can manually copy into blender plugin directory, but how to automate this? + * Does Blender offer alternatives e.g. configuring plugin path via env var? + * Look into creating additional packages, maybe as empty packages. +* Openshift Cuebot version + * Putting multiple Cuebots behind gRPC load balancer, and pointing RQD at the LB. Currently to + take a Cuebot offline all RQDs need to be restarted to move to a new Cuebot host, this solves + that problem. + * Would make a good tutorial or sample to include in the main repo. + * Prometheus export needs to be reworked. Currently using a separate client to query metrics, + which doesn't work with the LB setup as it will not redirect requests to a consistent Cuebot. + Working on a change to send metrics directly from Cuebot.