[Terminal Output] bug fixes to text sizer and missed lines rendered issue. (#142524)

* removed complex lines per frame logic. caused too many edge cases. tests added to prevent future regressions

* fix fit to screen option (when changing from fullscreen to not. also button state). increased playback speed to make up for removal of multi line per frame rendering

* fixed tests

* removing tty loading technique due to problems with unique char_device in multi container sessions on the same pod

Co-authored-by: Karl Godard <karlgodard@elastic.co>
This commit is contained in:
Karl Godard 2022-10-03 18:38:00 -07:00 committed by GitHub
parent b3a749e55a
commit 6de0091178
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 64 additions and 127 deletions

View file

@ -48,8 +48,7 @@ export const ALERT_STATUS = {
export const LOCAL_STORAGE_DISPLAY_OPTIONS_KEY = 'sessionView:displayOptions';
export const MOUSE_EVENT_PLACEHOLDER = { stopPropagation: () => undefined } as React.MouseEvent;
export const DEBOUNCE_TIMEOUT = 500;
export const DEFAULT_TTY_PLAYSPEED_MS = 50; // milliseconds per render loop
export const TTY_LINES_PER_FRAME = 5; // number of lines to print to xterm on each render loop
export const DEFAULT_TTY_PLAYSPEED_MS = 30; // milliseconds per render loop
export const TTY_LINES_PRE_SEEK = 200; // number of lines to redraw before the point we are seeking to.
export const DEFAULT_TTY_FONT_SIZE = 11;
export const DEFAULT_TTY_ROWS = 66;

View file

@ -39,6 +39,8 @@ describe('SessionView component', () => {
dispatchEvent: jest.fn(),
})),
});
global.ResizeObserver = require('resize-observer-polyfill');
});
beforeEach(() => {

View file

@ -8,11 +8,7 @@ import { renderHook, act } from '@testing-library/react-hooks';
import { sessionViewIOEventsMock } from '../../../common/mocks/responses/session_view_io_events.mock';
import { useIOLines, useXtermPlayer, XtermPlayerDeps } from './hooks';
import { ProcessEventsPage } from '../../../common/types/process_tree';
import {
DEFAULT_TTY_FONT_SIZE,
DEFAULT_TTY_PLAYSPEED_MS,
TTY_LINES_PER_FRAME,
} from '../../../common/constants';
import { DEFAULT_TTY_FONT_SIZE, DEFAULT_TTY_PLAYSPEED_MS } from '../../../common/constants';
const VIM_LINE_START = 22;
@ -132,9 +128,7 @@ describe('TTYPlayer/hooks', () => {
jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * 10);
});
const expectedLineNumber = Math.min(initialProps.lines.length - 1, TTY_LINES_PER_FRAME * 10);
expect(result.current.currentLine).toBe(expectedLineNumber);
expect(result.current.currentLine).toBe(10);
});
it('allows the user to stop', async () => {
@ -150,9 +144,7 @@ describe('TTYPlayer/hooks', () => {
act(() => {
jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * 10);
});
const expectedLineNumber = Math.min(initialProps.lines.length - 1, TTY_LINES_PER_FRAME * 10);
expect(result.current.currentLine).toBe(expectedLineNumber); // should not have advanced
expect(result.current.currentLine).toBe(10); // should not have advanced
});
it('should stop when it reaches the end of the array of lines', async () => {
@ -182,6 +174,39 @@ describe('TTYPlayer/hooks', () => {
expect(result.current.terminal.buffer.active.getLine(0)?.translateToString(true)).toBe('256');
});
it('ensure the first few render loops have printed the right lines', async () => {
const { result, rerender } = renderHook((props) => useXtermPlayer(props), {
initialProps,
});
const LOOPS = 6;
rerender({ ...initialProps, isPlaying: true });
act(() => {
// advance render loop
jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * LOOPS);
});
rerender({ ...initialProps, isPlaying: false });
expect(result.current.terminal.buffer.active.getLine(0)?.translateToString(true)).toBe('256');
expect(result.current.terminal.buffer.active.getLine(1)?.translateToString(true)).toBe(',');
expect(result.current.terminal.buffer.active.getLine(2)?.translateToString(true)).toBe(
' Some Companies Puppet instance'
);
expect(result.current.terminal.buffer.active.getLine(3)?.translateToString(true)).toBe(
' | | | CentOS Stream release 8 on x86_64'
);
expect(result.current.terminal.buffer.active.getLine(4)?.translateToString(true)).toBe(
' *********************** Load average: 1.23, 1.01, 0.63'
);
expect(result.current.terminal.buffer.active.getLine(5)?.translateToString(true)).toBe(
' ************************ '
);
expect(result.current.currentLine).toBe(LOOPS);
});
it('will allow a plain text search highlight on the last line printed', async () => {
const { result: xTermResult } = renderHook((props) => useXtermPlayer(props), {
initialProps,

View file

@ -29,7 +29,6 @@ import {
DEFAULT_TTY_ROWS,
DEFAULT_TTY_COLS,
TTY_LINE_SPLITTER_REGEX,
TTY_LINES_PER_FRAME,
TTY_LINES_PRE_SEEK,
} from '../../../common/constants';
@ -226,6 +225,7 @@ export const useXtermPlayer = ({
if (clear) {
linesToPrint = lines.slice(Math.max(0, lineNumber - TTY_LINES_PRE_SEEK), lineNumber + 1);
try {
terminal.reset();
terminal.clear();
@ -234,7 +234,7 @@ export const useXtermPlayer = ({
// there is some random race condition with the jump to feature that causes these calls to error out.
}
} else {
linesToPrint = lines.slice(lineNumber, lineNumber + TTY_LINES_PER_FRAME);
linesToPrint = lines.slice(lineNumber, lineNumber + 1);
}
linesToPrint.forEach((line, index) => {
@ -243,7 +243,7 @@ export const useXtermPlayer = ({
}
});
},
[terminal, lines]
[lines, terminal]
);
useEffect(() => {
@ -284,9 +284,9 @@ export const useXtermPlayer = ({
if (!hasNextPage && currentLine === lines.length - 1) {
setIsPlaying(false);
} else {
const nextLine = Math.min(lines.length - 1, currentLine + TTY_LINES_PER_FRAME);
setCurrentLine(nextLine);
const nextLine = Math.min(lines.length - 1, currentLine + 1);
render(nextLine, false);
setCurrentLine(nextLine);
}
}, playSpeed);

View file

@ -28,6 +28,8 @@ describe('TTYPlayer component', () => {
dispatchEvent: jest.fn(),
})),
});
global.ResizeObserver = require('resize-observer-polyfill');
});
let render: () => ReturnType<AppContextTestRender['render']>;

View file

@ -13,6 +13,7 @@ import {
EuiButton,
EuiBetaBadge,
} from '@elastic/eui';
import useResizeObserver from 'use-resize-observer';
import { throttle } from 'lodash';
import { ProcessEvent } from '../../../common/types/process_tree';
import { TTYSearchBar } from '../tty_search_bar';
@ -45,7 +46,7 @@ export const TTYPlayer = ({
autoSeekToEntityId,
}: TTYPlayerDeps) => {
const ref = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const { ref: scrollRef, height: containerHeight = 1 } = useResizeObserver<HTMLDivElement>({});
const { data, fetchNextPage, hasNextPage, isFetching, refetch } =
useFetchIOEvents(sessionEntityId);
@ -188,7 +189,7 @@ export const TTYPlayer = ({
textSizer={
<TTYTextSizer
tty={tty}
containerHeight={scrollRef?.current?.offsetHeight || 0}
containerHeight={containerHeight}
fontSize={fontSize}
onFontSizeChanged={setFontSize}
isFullscreen={isFullscreen}

View file

@ -79,9 +79,15 @@ describe('TTYTextSizer component', () => {
it('emits a font size to fit to full screen, when isFullscreen = true', async () => {
renderResult = mockedContext.render(
<TTYTextSizer {...props} isFullscreen={true} containerHeight={400} />
<TTYTextSizer {...props} isFullscreen containerHeight={400} />
);
const zoomFitBtn = renderResult.queryByTestId('sessionView:TTYZoomFit');
if (zoomFitBtn) {
userEvent.click(zoomFitBtn);
}
expect(props.onFontSizeChanged).toHaveBeenCalledTimes(1);
expect(props.onFontSizeChanged).toHaveBeenCalledWith(FULL_SCREEN_FONT_SIZE);
});

View file

@ -65,13 +65,7 @@ export const TTYTextSizer = ({
onFontSizeChanged(newSize);
}
}
}, [containerHeight, fit, fontSize, onFontSizeChanged, tty?.rows]);
useEffect(() => {
if (isFullscreen) {
setFit(true);
}
}, [isFullscreen]);
}, [isFullscreen, containerHeight, fit, fontSize, onFontSizeChanged, tty?.rows]);
const onToggleFit = useCallback(() => {
const newValue = !fit;
@ -100,7 +94,8 @@ export const TTYTextSizer = ({
display={fit ? 'fill' : 'empty'}
iconType={fit ? 'expand' : 'minimize'}
onClick={onToggleFit}
{...commonButtonProps}
size="s"
color="ghost"
/>
</EuiToolTip>
</EuiFlexItem>

View file

@ -4,16 +4,13 @@
*/
import { schema } from '@kbn/config-schema';
import { IRouter } from '@kbn/core/server';
import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils';
import { EVENT_ACTION } from '@kbn/rule-data-utils';
import {
GET_TOTAL_IO_BYTES_ROUTE,
PROCESS_EVENTS_INDEX,
TOTAL_BYTES_CAPTURED_PROPERTY,
TTY_CHAR_DEVICE_MAJOR_PROPERTY,
TTY_CHAR_DEVICE_MINOR_PROPERTY,
HOST_ID_PROPERTY,
ENTRY_SESSION_ENTITY_ID_PROPERTY,
} from '../../common/constants';
import { getTTYQueryPredicates } from './io_events_route';
export const registerGetTotalIOBytesRoute = (router: IRouter) => {
router.get(
@ -30,30 +27,14 @@ export const registerGetTotalIOBytesRoute = (router: IRouter) => {
const { sessionEntityId } = request.query;
try {
const ttyPredicates = await getTTYQueryPredicates(client, sessionEntityId);
if (!ttyPredicates) {
return response.ok({ body: { total: 0 } });
}
const search = await client.search({
index: [PROCESS_EVENTS_INDEX],
body: {
query: {
bool: {
must: [
{ term: { [TTY_CHAR_DEVICE_MAJOR_PROPERTY]: ttyPredicates.ttyMajor } },
{ term: { [TTY_CHAR_DEVICE_MINOR_PROPERTY]: ttyPredicates.ttyMinor } },
{ term: { [HOST_ID_PROPERTY]: ttyPredicates.hostId } },
{ term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } },
{ term: { [EVENT_ACTION]: 'text_output' } },
{
range: {
[TIMESTAMP]: {
gte: ttyPredicates.range[0],
lte: ttyPredicates.range[1],
},
},
},
],
},
},

View file

@ -8,75 +8,17 @@ import { schema } from '@kbn/config-schema';
import { IRouter } from '@kbn/core/server';
import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils';
import type { ElasticsearchClient } from '@kbn/core/server';
import { parse } from '@kbn/datemath';
import { Aggregate } from '../../common/types/aggregate';
import { EventAction, EventKind, ProcessEvent } from '../../common/types/process_tree';
import { EventAction, EventKind } from '../../common/types/process_tree';
import {
IO_EVENTS_ROUTE,
IO_EVENTS_PER_PAGE,
PROCESS_EVENTS_INDEX,
ENTRY_SESSION_ENTITY_ID_PROPERTY,
TTY_CHAR_DEVICE_MAJOR_PROPERTY,
TTY_CHAR_DEVICE_MINOR_PROPERTY,
HOST_ID_PROPERTY,
PROCESS_ENTITY_ID_PROPERTY,
PROCESS_EVENTS_PER_PAGE,
} from '../../common/constants';
/**
* Grabs the most recent event for the session and extracts the TTY char_device
* major/minor numbers, boot id, and session date range to use in querying for tty IO events.
* This is done so that any process from any session that writes to this TTY at the time of
* this session will be shown in the TTY Player. e.g. wall
*/
export const getTTYQueryPredicates = async (
client: ElasticsearchClient,
sessionEntityId: string
) => {
const lastEventQuery = await client.search({
index: [PROCESS_EVENTS_INDEX],
body: {
query: {
bool: {
minimum_should_match: 1,
should: [
{ term: { [EVENT_ACTION]: 'fork' } },
{ term: { [EVENT_ACTION]: 'exec' } },
{ term: { [EVENT_ACTION]: 'end' } },
{ term: { [EVENT_ACTION]: 'text_output' } },
],
must: [{ term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } }],
},
},
size: 1,
sort: [{ [TIMESTAMP]: 'desc' }],
},
});
const lastEventHits = lastEventQuery.hits.hits;
if (lastEventHits.length > 0) {
const lastEvent: ProcessEvent = lastEventHits[0]._source as ProcessEvent;
const lastEventTime = lastEvent['@timestamp'];
const rangeEnd =
(lastEventTime && parse(lastEventTime)?.toISOString()) || new Date().toISOString();
const range = [lastEvent?.process?.entry_leader?.start, rangeEnd];
const tty = lastEvent?.process?.entry_leader?.tty;
const hostId = lastEvent?.host?.id;
if (tty?.char_device?.major !== undefined && tty?.char_device?.minor !== undefined && hostId) {
return {
ttyMajor: tty.char_device.major,
ttyMinor: tty.char_device.minor,
hostId,
range,
};
}
}
return null;
};
export const registerIOEventsRoute = (router: IRouter) => {
router.get(
{
@ -94,30 +36,14 @@ export const registerIOEventsRoute = (router: IRouter) => {
const { sessionEntityId, cursor, pageSize = IO_EVENTS_PER_PAGE } = request.query;
try {
const ttyPredicates = await getTTYQueryPredicates(client, sessionEntityId);
if (!ttyPredicates) {
return response.ok({ body: { total: 0, events: [] } });
}
const search = await client.search({
index: [PROCESS_EVENTS_INDEX],
body: {
query: {
bool: {
must: [
{ term: { [TTY_CHAR_DEVICE_MAJOR_PROPERTY]: ttyPredicates.ttyMajor } },
{ term: { [TTY_CHAR_DEVICE_MINOR_PROPERTY]: ttyPredicates.ttyMinor } },
{ term: { [HOST_ID_PROPERTY]: ttyPredicates.hostId } },
{ term: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId } },
{ term: { [EVENT_ACTION]: 'text_output' } },
{
range: {
[TIMESTAMP]: {
gte: ttyPredicates.range[0]?.toString(),
lte: ttyPredicates.range[1]?.toString(),
},
},
},
],
},
},