Line data Source code
1 : /*
2 : * This file is part of Cockpit.
3 : *
4 : * Copyright (C) 2020 Red Hat, Inc.
5 : *
6 : * Cockpit is free software; you can redistribute it and/or modify it
7 : * under the terms of the GNU Lesser General Public License as published by
8 : * the Free Software Foundation; either version 2.1 of the License, or
9 : * (at your option) any later version.
10 : *
11 : * Cockpit is distributed in the hope that it will be useful, but
12 : * WITHOUT ANY WARRANTY; without even the implied warranty of
13 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 : * Lesser General Public License for more details.
15 : *
16 : * You should have received a copy of the GNU Lesser General Public License
17 : * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
18 : */
19 :
20 43 : import React from 'react';
21 :
22 : import { Button } from "@patternfly/react-core/dist/esm/components/Button";
23 : import { ExclamationCircleIcon } from '@patternfly/react-icons';
24 43 : import { CanvasAddon } from '@xterm/addon-canvas';
25 43 : import { Terminal } from "@xterm/xterm";
26 43 : import PropTypes from 'prop-types';
27 :
28 : import cockpit from 'cockpit';
29 : import { EmptyStatePanel } from "cockpit-components-empty-state.tsx";
30 :
31 : import * as client from './client.js';
32 : import rest from './rest.js';
33 :
34 : import "./ContainerTerminal.css";
35 :
36 43 : const _ = cockpit.gettext;
37 :
38 43 : class ContainerLogs extends React.Component {
39 6 : constructor(props) {
40 6 : super(props);
41 :
42 6 : this.onStreamClose = this.onStreamClose.bind(this);
43 6 : this.onStreamMessage = this.onStreamMessage.bind(this);
44 6 : this.connectStream = this.connectStream.bind(this);
45 :
46 6 : this.view = new Terminal({
47 6 : cols: 80,
48 6 : rows: 24,
49 6 : convertEol: true,
50 6 : cursorBlink: false,
51 6 : disableStdin: true,
52 6 : fontSize: 12,
53 6 : fontFamily: 'Menlo, Monaco, Consolas, monospace',
54 6 : screenReaderMode: true
55 6 : });
56 6 : this.view._core.cursorHidden = true;
57 6 : this.view.write(_("Loading logs..."));
58 :
59 6 : this.logRef = React.createRef();
60 :
61 6 : this.state = {
62 6 : opened: false,
63 6 : loading: true,
64 6 : errorMessage: "",
65 6 : streamer: null,
66 6 : };
67 6 : }
68 :
69 6 : componentDidMount() {
70 6 : this._ismounted = true;
71 6 : this.connectStream();
72 6 : }
73 :
74 6 : componentDidUpdate(prevProps) {
75 : // Connect channel when there is none and container started
76 2 : if (!this.state.streamer && this.props.containerStatus === "running" && prevProps.containerStatus !== "running")
77 2 : this.connectStream();
78 0 : if (prevProps.width !== this.props.width) {
79 0 : this.resize(this.props.width);
80 0 : }
81 6 : }
82 :
83 6 : resize(width) {
84 0 : if (!this.term?._core?._renderService?.dimensions)
85 6 : return;
86 : // 24 PF padding * 4
87 : // 3 line border
88 : // 21 inner padding of xterm.js
89 : // xterm.js scrollbar 20
90 0 : const padding = 24 * 4 + 3 + 21 + 20;
91 : // missing API: https://github.com/xtermjs/xterm.js/issues/702
92 0 : const realWidth = this.view._core._renderService.dimensions.css.cell.width;
93 0 : const cols = Math.floor((width - padding) / realWidth);
94 0 : this.view.resize(cols, 24);
95 6 : }
96 :
97 2 : componentWillUnmount() {
98 2 : this._ismounted = false;
99 2 : if (this.state.streamer)
100 2 : this.state.streamer.close();
101 2 : this.view.dispose();
102 2 : }
103 :
104 6 : connectStream() {
105 6 : if (this.state.streamer !== null)
106 6 : return;
107 :
108 : // Show the terminal. Once it was shown, do not show it again but reuse the previous one
109 6 : if (!this.state.opened) {
110 6 : this.view.open(this.logRef.current);
111 6 : this.view.loadAddon(new CanvasAddon());
112 6 : this.setState({ opened: true });
113 6 : }
114 6 : this.resize(this.props.width);
115 :
116 6 : const connection = rest.connect(this.props.uid);
117 6 : connection.monitor(client.VERSION + "libpod/containers/" + this.props.containerId +
118 6 : "/logs?follow=true&stdout=true&stderr=true",
119 6 : this.onStreamMessage, true)
120 6 : .then(this.onStreamClose)
121 1 : .catch(e => {
122 1 : const error = JSON.parse(new TextDecoder().decode(e.message));
123 1 : this.setState({
124 1 : errorMessage: error.message,
125 1 : streamer: null,
126 1 : });
127 1 : });
128 6 : this.setState({
129 6 : streamer: connection,
130 6 : errorMessage: "",
131 6 : });
132 6 : }
133 :
134 5 : onStreamMessage(data) {
135 5 : if (data) {
136 5 : if (this.state.loading) {
137 5 : this.view.reset();
138 5 : this.view._core.cursorHidden = true;
139 5 : this.setState({ loading: false });
140 5 : }
141 : // First 8 bytes encode information about stream and frame
142 : // See 'Stream format' on https://docs.docker.com/engine/api/v1.40/#operation/ContainerAttach
143 4 : while (data.byteLength >= 8) {
144 : // split into frames (size is the second 32-bit word)
145 4 : const size = data[7] + data[6] * 0x100 + data[5] * 0x10000 + data[4] * 0x1000000;
146 4 : const frame = data.slice(8, 8 + size);
147 : // old podman versions just have CR endings, append NL then
148 4 : if (frame[size - 1] === 13)
149 0 : this.view.writeln(frame);
150 : else
151 : // recent podman versions have CRNL endings
152 4 : this.view.write(frame);
153 4 : data = data.slice(8 + size);
154 4 : }
155 5 : }
156 5 : }
157 :
158 4 : onStreamClose() {
159 2 : if (this._ismounted) {
160 2 : this.setState({
161 2 : streamer: null,
162 2 : });
163 2 : this.view.write("Streaming disconnected");
164 2 : }
165 4 : }
166 :
167 6 : render() {
168 6 : let element = <div className="container-logs" ref={this.logRef} />;
169 6 : const { systemd_unit, uid } = this.props;
170 :
171 1 : if (this.state.errorMessage) {
172 1 : element = <EmptyStatePanel icon={ExclamationCircleIcon} title={this.state.errorMessage} />;
173 0 : } else if (systemd_unit !== undefined && uid === 0) {
174 1 : element = (
175 1 : <>
176 1 : {element}
177 1 : <Button variant="link" isInline className="pf-v6-u-mt-sm" onClick={
178 1 : () => cockpit.jump(`/system/logs/#/?priority=info&_SYSTEMD_UNIT=${systemd_unit}`)}>
179 1 : {cockpit.format(_("View $0 logs"), systemd_unit)}
180 1 : </Button>
181 1 : </>
182 : );
183 1 : }
184 :
185 6 : return element;
186 6 : }
187 43 : }
188 :
189 43 : ContainerLogs.propTypes = {
190 43 : containerId: PropTypes.string.isRequired,
191 43 : uid: PropTypes.number,
192 43 : width: PropTypes.number.isRequired,
193 43 : systemd_unit: PropTypes.string
194 43 : };
195 :
196 43 : export default ContainerLogs;
|