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 \