LCOV - code coverage report
Current view: top level - src - ContainerLogs.jsx Coverage Total Hit
Test: cockpit-podman Lines: 92.4 % 132 122
Test Date: 2025-05-21 17:35:03

            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;
        

Generated by: LCOV version 2.0-1