diff --git a/awx/main/tasks/receptor.py b/awx/main/tasks/receptor.py index 639abf671f..94568ebd6c 100644 --- a/awx/main/tasks/receptor.py +++ b/awx/main/tasks/receptor.py @@ -411,9 +411,11 @@ class AWXReceptorJob: unit_status = receptor_ctl.simple_command(f'work status {self.unit_id}') detail = unit_status.get('Detail', None) state_name = unit_status.get('StateName', None) + stdout_size = unit_status.get('StdoutSize', 0) except Exception: detail = '' state_name = '' + stdout_size = 0 logger.exception(f'An error was encountered while getting status for work unit {self.unit_id}') if 'exceeded quota' in detail: @@ -424,9 +426,16 @@ class AWXReceptorJob: return try: - resultsock = receptor_ctl.get_work_results(self.unit_id, return_sockfile=True) - lines = resultsock.readlines() - receptor_output = b"".join(lines).decode() + receptor_output = '' + if state_name == 'Failed' and self.task.runner_callback.event_ct == 0: + # if receptor work unit failed and no events were emitted, work results may + # contain useful information about why the job failed. In case stdout is + # massive, only ask for last 1000 bytes + startpos = max(stdout_size - 1000, 0) + resultsock, resultfile = receptor_ctl.get_work_results(self.unit_id, startpos=startpos, return_socket=True, return_sockfile=True) + resultsock.setblocking(False) # this makes resultfile reads non blocking + lines = resultfile.readlines() + receptor_output = b"".join(lines).decode() if receptor_output: self.task.runner_callback.delay_update(result_traceback=receptor_output) elif detail: diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index 1740a4c8f6..7f7116d78b 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -103,6 +103,10 @@ ColorHandler = logging.StreamHandler if settings.COLOR_LOGS is True: try: from logutils.colorize import ColorizingStreamHandler + import colorama + + colorama.deinit() + colorama.init(wrap=False, convert=False, strip=False) class ColorHandler(ColorizingStreamHandler): def colorize(self, line, record): diff --git a/awx/ui/src/components/Search/AdvancedSearch.test.js b/awx/ui/src/components/Search/AdvancedSearch.test.js index 5050ff63af..8258ef6812 100644 --- a/awx/ui/src/components/Search/AdvancedSearch.test.js +++ b/awx/ui/src/components/Search/AdvancedSearch.test.js @@ -420,7 +420,7 @@ describe('', () => { const selectOptions = wrapper.find( 'Select[aria-label="Related search type"] SelectOption' ); - expect(selectOptions).toHaveLength(2); + expect(selectOptions).toHaveLength(3); expect( selectOptions.find('SelectOption[id="name-option-select"]').prop('value') ).toBe('name__icontains'); diff --git a/awx/ui/src/components/Search/RelatedLookupTypeInput.js b/awx/ui/src/components/Search/RelatedLookupTypeInput.js index effbc4199a..008c83164b 100644 --- a/awx/ui/src/components/Search/RelatedLookupTypeInput.js +++ b/awx/ui/src/components/Search/RelatedLookupTypeInput.js @@ -31,6 +31,12 @@ function RelatedLookupTypeInput({ value="name__icontains" description={t`Fuzzy search on name field.`} /> + - {lineNumber} + {!event.isTracebackOnly ? lineNumber : ''} { const pendingRequests = Object.values(eventByUuidRequests.current || {}); setHasContentLoading(true); // prevents "no content found" screen from flashing - setIsFollowModeEnabled(false); + if (location.search) { + setIsFollowModeEnabled(false); + } Promise.allSettled(pendingRequests).then(() => { setRemoteRowCount(0); clearLoadedEvents(); @@ -251,6 +253,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { }); const updated = oldWsEvents.concat(newEvents); jobSocketCounter.current = updated.length; + if (!oldWsEvents.length && min > remoteRowCount + 1) { + loadJobEvents(min); + } return updated.sort((a, b) => a.counter - b.counter); }); setCssMap((prevCssMap) => ({ @@ -358,7 +363,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { } }; - const loadJobEvents = async () => { + const loadJobEvents = async (firstWsCounter = null) => { const [params, loadRange] = getEventRequestParams(job, 50, [1, 50]); if (isMounted.current) { @@ -371,6 +376,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { if (isFlatMode) { params.not__stdout = ''; } + if (firstWsCounter) { + params.counter__lt = firstWsCounter; + } const qsParams = parseQueryString(QS_CONFIG, location.search); const eventPromise = getJobModel(job.type).readEvents(job.id, { ...params, @@ -435,7 +443,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { if (getEvent(counter)) { return true; } - if (index > remoteRowCount && index < remoteRowCount + wsEvents.length) { + if (index >= remoteRowCount && index < remoteRowCount + wsEvents.length) { return true; } return currentlyLoading.includes(counter); @@ -462,7 +470,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { } if ( !event && - index > remoteRowCount && + index >= remoteRowCount && index < remoteRowCount + wsEvents.length ) { event = wsEvents[index - remoteRowCount]; @@ -629,10 +637,14 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { setIsFollowModeEnabled(false); }; - const scrollToEnd = () => { + const scrollToEnd = useCallback(() => { scrollToRow(-1); - setTimeout(() => scrollToRow(-1), 100); - }; + let timeout; + if (isFollowModeEnabled) { + setTimeout(() => scrollToRow(-1), 100); + } + return () => clearTimeout(timeout); + }, [isFollowModeEnabled]); const handleScrollLast = () => { scrollToEnd(); diff --git a/awx/ui/src/screens/Job/JobOutput/loadJobEvents.js b/awx/ui/src/screens/Job/JobOutput/loadJobEvents.js index f6300d4525..1dd59c607f 100644 --- a/awx/ui/src/screens/Job/JobOutput/loadJobEvents.js +++ b/awx/ui/src/screens/Job/JobOutput/loadJobEvents.js @@ -29,8 +29,11 @@ export function prependTraceback(job, events) { start_line: 0, }; const firstIndex = events.findIndex((jobEvent) => jobEvent.counter === 1); - if (firstIndex && events[firstIndex]?.stdout) { - const stdoutLines = events[firstIndex].stdout.split('\r\n'); + if (firstIndex > -1) { + if (!events[firstIndex].stdout) { + events[firstIndex].isTracebackOnly = true; + } + const stdoutLines = events[firstIndex].stdout?.split('\r\n') || []; stdoutLines[0] = tracebackEvent.stdout; events[firstIndex].stdout = stdoutLines.join('\r\n'); } else { diff --git a/awx/ui/src/screens/Setting/Jobs/JobsEdit/JobsEdit.js b/awx/ui/src/screens/Setting/Jobs/JobsEdit/JobsEdit.js index 6ae68c1c8d..52e216e41e 100644 --- a/awx/ui/src/screens/Setting/Jobs/JobsEdit/JobsEdit.js +++ b/awx/ui/src/screens/Setting/Jobs/JobsEdit/JobsEdit.js @@ -141,14 +141,14 @@ function JobsEdit() { ', () => { await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); expect(wrapper.find('ContentError').length).toBe(1); }); + + test('Form input fields that are invisible (due to being set manually via a settings file) should not prevent submitting the form', async () => { + const mockOptions = Object.assign({}, mockAllOptions); + // If AWX_ISOLATION_BASE_PATH has been set in a settings file it will be absent in the PUT options + delete mockOptions['actions']['PUT']['AWX_ISOLATION_BASE_PATH']; + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); }); diff --git a/awx/ui/src/screens/Setting/shared/SharedFields.js b/awx/ui/src/screens/Setting/shared/SharedFields.js index 9fd1817bb8..06851e3b9e 100644 --- a/awx/ui/src/screens/Setting/shared/SharedFields.js +++ b/awx/ui/src/screens/Setting/shared/SharedFields.js @@ -397,7 +397,10 @@ const InputField = ({ name, config, type = 'text', isRequired = false }) => { }; InputField.propTypes = { name: string.isRequired, - config: shape({}).isRequired, + config: shape({}), +}; +InputField.defaultProps = { + config: null, }; const TextAreaField = ({ name, config, isRequired = false }) => { diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index a83a130283..166f24b79f 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -116,7 +116,7 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \ python3-psycopg2 \ python3-setuptools \ rsync \ - "rsyslog >= 8.1911.0" \ + rsyslog-8.2102.0-106.el9 \ subversion \ sudo \ vim-minimal \