diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go index 244fdea4e0c..9b86c86d821 100644 --- a/lib/auth/apiserver.go +++ b/lib/auth/apiserver.go @@ -144,8 +144,8 @@ func NewAPIServer(config *APIConfig) (http.Handler, error) { srv.POST("/:version/tokens/register", srv.WithAuth(srv.registerUsingToken)) // Active sessions - srv.GET("/:version/namespaces/:namespace/sessions/:id/stream", srv.WithAuth(srv.getSessionChunk)) - srv.GET("/:version/namespaces/:namespace/sessions/:id/events", srv.WithAuth(srv.getSessionEvents)) + srv.GET("/:version/namespaces/:namespace/sessions/:id/stream", srv.WithAuth(srv.getSessionChunk)) // DELETE IN 16(zmb3) + srv.GET("/:version/namespaces/:namespace/sessions/:id/events", srv.WithAuth(srv.getSessionEvents)) // DELETE IN 16(zmb3) // Namespaces srv.POST("/:version/namespaces", srv.WithAuth(srv.upsertNamespace)) diff --git a/lib/player/player.go b/lib/player/player.go index 0a579685223..9af0517f0dd 100644 --- a/lib/player/player.go +++ b/lib/player/player.go @@ -116,7 +116,7 @@ func New(cfg *Config) (*Player, error) { log: log, sessionID: cfg.SessionID, streamer: cfg.Streamer, - emit: make(chan events.AuditEvent, 64), + emit: make(chan events.AuditEvent, 1024), playPause: make(chan chan struct{}, 1), done: make(chan struct{}), } @@ -185,7 +185,7 @@ func (p *Player) stream() { } currentDelay := getDelay(evt) - if currentDelay > 0 && currentDelay > lastDelay { + if currentDelay > 0 && currentDelay >= lastDelay { switch adv := p.advanceTo.Load(); { case adv >= currentDelay: // no timing delay necessary, we are fast forwarding @@ -215,12 +215,13 @@ func (p *Player) stream() { lastDelay = currentDelay } - select { - case p.emit <- evt: - p.lastPlayed.Store(currentDelay) - default: - p.log.Warnf("dropped event %v, reader too slow", evt.GetID()) - } + // if the receiver can't keep up, let the channel throttle us + // (it's better for playback to be a little slower than realtime + // than to drop events) + // + // TODO: consider a select with a timeout to detect blocked readers? + p.emit <- evt + p.lastPlayed.Store(currentDelay) } } } diff --git a/lib/player/player_test.go b/lib/player/player_test.go index 6727fd5b543..44414ad83eb 100644 --- a/lib/player/player_test.go +++ b/lib/player/player_test.go @@ -169,6 +169,7 @@ func TestClose(t *testing.T) { _, ok := <-p.C() require.False(t, ok, "player channel should have been closed") require.NoError(t, p.Err()) + require.Equal(t, int64(1000), p.LastPlayed()) } func TestSeekForward(t *testing.T) { diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index b721fe966f3..250ce935909 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -717,8 +717,11 @@ func (h *Handler) bindDefaultEndpoints() { // Audit events handlers. h.GET("/webapi/sites/:site/events/search", h.WithClusterAuth(h.clusterSearchEvents)) // search site events h.GET("/webapi/sites/:site/events/search/sessions", h.WithClusterAuth(h.clusterSearchSessionEvents)) // search site session events - h.GET("/webapi/sites/:site/sessions/:sid/events", h.WithClusterAuth(h.siteSessionEventsGet)) // get recorded session's timing information (from events) - h.GET("/webapi/sites/:site/sessions/:sid/stream", h.siteSessionStreamGet) // get recorded session's bytes (from events) + h.GET("/webapi/sites/:site/ttyplayback/:sid", h.WithClusterAuth(h.ttyPlaybackHandle)) + + // DELETE in 16(zmb3): v15+ web UIs use new streaming 'ttyplayback' endpoint + h.GET("/webapi/sites/:site/sessions/:sid/events", h.WithClusterAuth(h.siteSessionEventsGet)) // get recorded session's timing information (from events) + h.GET("/webapi/sites/:site/sessions/:sid/stream", h.siteSessionStreamGet) // get recorded session's bytes (from events) // scp file transfer h.GET("/webapi/sites/:site/nodes/:server/:login/scp", h.WithClusterAuth(h.transferFile)) @@ -3417,6 +3420,8 @@ func queryOrder(query url.Values, name string, def types.EventOrder) (types.Even // It returns the binary stream unencoded, directly in the respose body, // with Content-Type of application/octet-stream, gzipped with up to 95% // compression ratio. +// +// DELETE IN 16(zmb3) func (h *Handler) siteSessionStreamGet(w http.ResponseWriter, r *http.Request, p httprouter.Params) { httplib.SetNoCacheHeaders(w.Header()) diff --git a/lib/web/tty_playback.go b/lib/web/tty_playback.go new file mode 100644 index 00000000000..3f04a14e0bf --- /dev/null +++ b/lib/web/tty_playback.go @@ -0,0 +1,358 @@ +/* + * Teleport + * Copyright (C) 2023 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package web + +import ( + "bytes" + "context" + "encoding/binary" + "net/http" + "time" + + "github.com/gorilla/websocket" + "github.com/gravitational/trace" + "github.com/julienschmidt/httprouter" + + "github.com/gravitational/teleport/api/types/events" + "github.com/gravitational/teleport/lib/player" + "github.com/gravitational/teleport/lib/reversetunnelclient" + "github.com/gravitational/teleport/lib/session" + "github.com/gravitational/teleport/lib/utils" +) + +const ( + messageTypePTY = byte(1) + messageTypeError = byte(2) + messageTypePlayPause = byte(3) + messageTypeSeek = byte(4) + messageTypeResize = byte(5) +) + +const ( + severityError = byte(1) +) + +const ( + actionPlay = byte(0) + actionPause = byte(1) +) + +func (h *Handler) ttyPlaybackHandle( + w http.ResponseWriter, + r *http.Request, + p httprouter.Params, + sctx *SessionContext, + site reversetunnelclient.RemoteSite, +) (interface{}, error) { + sID := p.ByName("sid") + if sID == "" { + return nil, trace.BadParameter("missing session ID in request URL") + } + + clt, err := sctx.GetUserClient(r.Context(), site) + if err != nil { + return nil, trace.Wrap(err) + } + + h.log.Debug("upgrading to websocket") + upgrader := websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + h.log.Warn("failed upgrade", err) + // if Upgrade fails, it automatically replies with an HTTP error + // (this means we don't need to return an error here) + return nil, nil + } + + player, err := player.New(&player.Config{ + Clock: h.clock, + Log: h.log, + SessionID: session.ID(sID), + Streamer: clt, + }) + if err != nil { + h.log.Warn("player error", err) + writeError(ws, err) + return nil, nil + } + + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + + go func() { + defer cancel() + for { + typ, b, err := ws.ReadMessage() + if err != nil { + if !utils.IsOKNetworkError(err) { + log.Warnf("websocket read error: %v", err) + } + return + } + + if typ != websocket.BinaryMessage { + log.Debugf("skipping unknown websocket message type %v", typ) + continue + } + + if err := handlePlaybackAction(b, player); err != nil { + log.Warnf("skipping bad action: %v", err) + continue + } + } + }() + + go func() { + defer cancel() + defer func() { + h.log.Debug("closing websocket") + if err := ws.WriteMessage(websocket.CloseMessage, nil); err != nil { + h.log.Debugf("error sending close message: %v", err) + } + if err := ws.Close(); err != nil { + h.log.Debugf("error closing websocket: %v", err) + } + }() + + player.Play() + defer player.Close() + + headerBuf := make([]byte, 11) + headerBuf[0] = messageTypePTY + + writePTY := func(b []byte, delay uint64) error { + writer, err := ws.NextWriter(websocket.BinaryMessage) + if err != nil { + return trace.Wrap(err, "getting websocket writer") + } + msgLen := uint16(len(b) + 8) + binary.BigEndian.PutUint16(headerBuf[1:], msgLen) + binary.BigEndian.PutUint64(headerBuf[3:], delay) + if _, err := writer.Write(headerBuf); err != nil { + return trace.Wrap(err, "writing message header") + } + + // TODO(zmb3): consider optimizing this by bufering for very large sessions + // (wait up to N ms to batch events into a single websocket write). + if _, err := writer.Write(b); err != nil { + return trace.Wrap(err, "writing PTY data") + } + + if err := writer.Close(); err != nil { + return trace.Wrap(err, "closing websocket writer") + } + + return nil + } + + writeSize := func(size string) error { + ts, err := session.UnmarshalTerminalParams(size) + if err != nil { + h.log.Debugf("Ignoring invalid terminal size %q", size) + return nil // don't abort playback due to a bad event + } + + msg := make([]byte, 7) + msg[0] = messageTypeResize + binary.BigEndian.PutUint16(msg[1:], 4) + binary.BigEndian.PutUint16(msg[3:], uint16(ts.W)) + binary.BigEndian.PutUint16(msg[5:], uint16(ts.H)) + + return trace.Wrap(ws.WriteMessage(websocket.BinaryMessage, msg)) + } + + for { + select { + case <-ctx.Done(): + return + case evt, ok := <-player.C(): + if !ok { + // send any playback errors to the browser + if err := writeError(ws, player.Err()); err != nil { + h.log.Warnf("failed to send error message to browser: %v", err) + } + return + } + + switch evt := evt.(type) { + case *events.SessionStart: + if err := writeSize(evt.TerminalSize); err != nil { + h.log.Debugf("Failed to write resize: %v", err) + return + } + + case *events.SessionPrint: + if err := writePTY(evt.Data, uint64(evt.DelayMilliseconds)); err != nil { + h.log.Debugf("Failed to send PTY data: %v", err) + return + } + + case *events.SessionEnd: + // send empty PTY data - this will ensure that any dead time + // at the end of the recording is processed and the allow + // the progress bar to go to 100% + if err := writePTY(nil, uint64(evt.EndTime.Sub(evt.StartTime)/time.Millisecond)); err != nil { + h.log.Debugf("Failed to send session end data: %v", err) + return + } + + case *events.Resize: + if err := writeSize(evt.TerminalSize); err != nil { + h.log.Debugf("Failed to write resize: %v", err) + return + } + + default: + h.log.Debugf("unexpected event type %T", evt) + } + } + } + }() + + <-ctx.Done() + return nil, nil +} + +func writeError(ws *websocket.Conn, err error) error { + if err == nil { + return nil + } + + b := new(bytes.Buffer) + b.WriteByte(messageTypeError) + + msg := trace.UserMessage(err) + l := 1 /* severity */ + 2 /* msg length */ + len(msg) + binary.Write(b, binary.BigEndian, uint16(l)) + b.WriteByte(severityError) + binary.Write(b, binary.BigEndian, uint16(len(msg))) + b.WriteString(msg) + + return trace.Wrap(ws.WriteMessage(websocket.BinaryMessage, b.Bytes())) +} + +type play interface { + Play() error + Pause() error + SetPos(time.Duration) error +} + +// handlePlaybackAction processes a playback message +// received from the browser +func handlePlaybackAction(b []byte, p play) error { + if len(b) < 3 { + return trace.BadParameter("invalid playback message") + } + + msgType := b[0] + msgLen := binary.BigEndian.Uint16(b[1:]) + + if len(b) < int(msgLen+3) { + return trace.BadParameter("invalid message length") + } + + payload := b[3:] + payload = payload[:msgLen] + + switch msgType { + case messageTypePlayPause: + if len(payload) != 1 { + return trace.BadParameter("invalid play/pause command") + } + switch action := payload[0]; action { + case actionPlay: + p.Play() + case actionPause: + p.Pause() + default: + return trace.BadParameter("invalid play/pause action %v", action) + } + case messageTypeSeek: + if len(payload) != 8 { + return trace.BadParameter("invalid seek message") + } + pos := binary.BigEndian.Uint64(payload) + p.SetPos(time.Duration(pos) * time.Millisecond) + } + + return nil +} + +/* + +# Websocket Protocol for TTY Playback: + +During playback, the Teleport proxy sends session data to the browser +and the browser sends playback commands (play/pause, seek, etc) to the +proxy. + +Each message conforms to the following binary protocol. + +## Message Header + +The message header starts with a 1-byte identifier followed by a 2-byte +(big endian) integer containing the number of bytes following the header. +This length field does not include the 3-byte header. + +## Messages + +### 1 - PTY data + +This message is used to send recorded PTY data to the browser. + +- Message ID: 1 +- 8-byte timestamp (milliseconds since session start) +- PTY data + +### 2 - Error + +This message is used to indicate that an error has occurred. + +- Message ID: 2 +- 1 byte severity (1=error) +- 2-byte error message length +- variable length error message (UTF-8 text) + +### 3 - Play/Pause + +This message is sent from the browser to the server to pause +or resume playback. + +- Message ID: 3 +- 1-byte code (0=play, 1=pause) + +### 4 - Seek + +This message is used to seek to a new position in the recording. + +- Message ID: 4 +- 8-byte timestamp (milliseconds since session start) + +### 5 - Resize + +This message is used to indicate that the termina was resized. + +- Message ID: 5 +- 2-byte width +- 2-byte height + +*/ diff --git a/lib/web/tty_playback_test.go b/lib/web/tty_playback_test.go new file mode 100644 index 00000000000..bbd575d979f --- /dev/null +++ b/lib/web/tty_playback_test.go @@ -0,0 +1,37 @@ +/* + * Teleport + * Copyright (C) 2023 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package web + +import ( + "testing" + "time" +) + +func FuzzHandlePlaybackAction(f *testing.F) { + player := nopPlayer{} + f.Fuzz(func(t *testing.T, b []byte) { + handlePlaybackAction(b, player) + }) +} + +type nopPlayer struct{} + +func (nopPlayer) SetPos(time.Duration) error { return nil } +func (nopPlayer) Play() error { return nil } +func (nopPlayer) Pause() error { return nil } diff --git a/web/packages/teleport/package.json b/web/packages/teleport/package.json index bd47f3c0379..e2588b5870a 100644 --- a/web/packages/teleport/package.json +++ b/web/packages/teleport/package.json @@ -25,11 +25,12 @@ "xterm-addon-web-links": "^0.8.0" }, "devDependencies": { - "babel-plugin-transform-import-meta": "^2.2.0", - "babel-plugin-transform-vite-meta-env": "^1.0.3", "@gravitational/build": "^1.0.0", "@types/wicg-file-system-access": "^2020.9.5", + "babel-plugin-transform-import-meta": "^2.2.0", + "babel-plugin-transform-vite-meta-env": "^1.0.3", "jest-canvas-mock": "^2.3.1", + "jest-websocket-mock": "^2.5.0", "ts-loader": "^9.4.2" } } diff --git a/web/packages/teleport/src/Assist/service.ts b/web/packages/teleport/src/Assist/service.ts index 6f0a6f4ff75..9897373e0d8 100644 --- a/web/packages/teleport/src/Assist/service.ts +++ b/web/packages/teleport/src/Assist/service.ts @@ -101,6 +101,8 @@ export async function resolveServerMessage( } } +// TODO(zmb3): check with Ryan about replacing this with streaming + export async function getSessionEvents(sessionUrl: string): Promise<{ events: SessionEvent[] | null; }> { diff --git a/web/packages/teleport/src/Player/Player.tsx b/web/packages/teleport/src/Player/Player.tsx index 2969f3de7b1..189f673c186 100644 --- a/web/packages/teleport/src/Player/Player.tsx +++ b/web/packages/teleport/src/Player/Player.tsx @@ -36,6 +36,8 @@ import { DesktopPlayer } from './DesktopPlayer'; import SshPlayer from './SshPlayer'; import Tabs, { TabItem } from './PlayerTabs'; +const validRecordingTypes = ['ssh', 'k8s', 'desktop']; + export default function Player() { const { sid, clusterId } = useParams(); const { search } = useLocation(); @@ -46,10 +48,7 @@ export default function Player() { ) as RecordingType; const durationMs = Number(getUrlParameter('durationMs', search)); - const validRecordingType = - recordingType === 'ssh' || - recordingType === 'k8s' || - recordingType === 'desktop'; + const validRecordingType = validRecordingTypes.includes(recordingType); const validDurationMs = Number.isInteger(durationMs) && durationMs > 0; document.title = `${clusterId} • Play ${sid}`; @@ -64,14 +63,14 @@ export default function Player() { Invalid query parameter recordingType: {recordingType}, should be - 'ssh' or 'desktop' + one of {validRecordingTypes.join(', ')}. ); } - if (recordingType === 'desktop' && !validDurationMs) { + if (!validDurationMs) { return ( @@ -106,7 +105,7 @@ export default function Player() { durationMs={durationMs} /> ) : ( - + )} diff --git a/web/packages/teleport/src/Player/ProgressBar/ProgressBar.tsx b/web/packages/teleport/src/Player/ProgressBar/ProgressBar.tsx index 403be4bcb9a..81aa8be12b6 100644 --- a/web/packages/teleport/src/Player/ProgressBar/ProgressBar.tsx +++ b/web/packages/teleport/src/Player/ProgressBar/ProgressBar.tsx @@ -26,7 +26,7 @@ export default function ProgressBar(props: ProgressBarProps) { const Icon = props.isPlaying ? Icons.CirclePause : Icons.CirclePlay; return ( - + @@ -36,7 +36,9 @@ export default function ProgressBar(props: ProgressBarProps) { min={props.min} max={props.max} value={props.current} - onChange={props.move} + disabled={props.disabled} + onBeforeChange={props.onStartMove} + onAfterChange={props.move} defaultValue={1} withBars className="grv-slider" @@ -67,13 +69,15 @@ function Restart(props: { onRestart?: () => void }) { export type ProgressBarProps = { max: number; min: number; - time: any; + time: string; isPlaying: boolean; + disabled?: boolean; current: number; move: (value: any) => void; toggle: () => void; style?: React.CSSProperties; id?: string; + onStartMove?: () => void; onPlaySpeedChange?: (newSpeed: number) => void; onRestart?: () => void; }; @@ -140,7 +144,13 @@ const ActionButton = styled.button` color: ${props => props.theme.colors.text.main}; } - &:hover { + &:disabled { + .icon { + color: ${props => props.theme.colors.text.disabled}; + } + } + + &:hover:enabled { opacity: 1; .icon { diff --git a/web/packages/teleport/src/Player/ProgressBar/ProgressBarTty.tsx b/web/packages/teleport/src/Player/ProgressBar/ProgressBarTty.tsx deleted file mode 100644 index e1b387c964f..00000000000 --- a/web/packages/teleport/src/Player/ProgressBar/ProgressBarTty.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import React from 'react'; -import { throttle } from 'shared/utils/highbar'; - -import TtyPlayer from 'teleport/lib/term/ttyPlayer'; - -import ProgressBar from './ProgressBar'; - -export default function ProgressBarTty(props: { tty: TtyPlayer }) { - const state = useTtyProgress(props.tty); - return ; -} - -export function useTtyProgress(tty: TtyPlayer) { - const [state, setState] = React.useState(() => { - return makeTtyProgress(tty); - }); - - React.useEffect(() => { - const throttledOnChange = throttle( - onChange, - // some magic numbers to reduce number of re-renders when - // session is too long and "eventful" - Math.max(Math.min(tty.duration * 0.025, 500), 20) - ); - - function onChange() { - // recalculate progress state - const ttyProgres = makeTtyProgress(tty); - setState(ttyProgres); - } - - function cleanup() { - throttledOnChange.cancel(); - tty.stop(); - tty.removeAllListeners(); - } - - tty.on('change', throttledOnChange); - - return cleanup; - }, [tty]); - - return state; -} - -function makeTtyProgress(tty: TtyPlayer) { - function toggle() { - if (tty.isPlaying()) { - tty.stop(); - } else { - tty.play(); - } - } - - function move(value) { - tty.move(value); - } - - return { - max: tty.duration, - min: 1, - time: tty.getCurrentTime(), - isLoading: tty.isLoading(), - isPlaying: tty.isPlaying(), - current: tty.current, - move, - toggle, - }; -} diff --git a/web/packages/teleport/src/Player/ProgressBar/Slider/Slider.jsx b/web/packages/teleport/src/Player/ProgressBar/Slider/Slider.jsx index 3e57a55645d..9ad8f1b9012 100644 --- a/web/packages/teleport/src/Player/ProgressBar/Slider/Slider.jsx +++ b/web/packages/teleport/src/Player/ProgressBar/Slider/Slider.jsx @@ -1,21 +1,3 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - /* The MIT License (MIT) diff --git a/web/packages/teleport/src/Player/ProgressBar/index.tsx b/web/packages/teleport/src/Player/ProgressBar/index.tsx index 9bfd7aa65de..9d892ade1a0 100644 --- a/web/packages/teleport/src/Player/ProgressBar/index.tsx +++ b/web/packages/teleport/src/Player/ProgressBar/index.tsx @@ -17,8 +17,7 @@ */ import ProgressBar from './ProgressBar'; -import ProgressBarTty from './ProgressBarTty'; import { ProgressBarDesktop } from './ProgressBarDesktop'; export default ProgressBar; -export { ProgressBarTty, ProgressBarDesktop }; +export { ProgressBarDesktop }; diff --git a/web/packages/teleport/src/Player/SshPlayer.tsx b/web/packages/teleport/src/Player/SshPlayer.tsx index 544230f2644..479f9237812 100644 --- a/web/packages/teleport/src/Player/SshPlayer.tsx +++ b/web/packages/teleport/src/Player/SshPlayer.tsx @@ -18,24 +18,27 @@ import React from 'react'; import styled from 'styled-components'; +import { Indicator, Flex, Box } from 'design'; import { Danger } from 'design/Alert'; -import { Indicator, Flex, Text, Box } from 'design'; import cfg from 'teleport/config'; import TtyPlayer, { + StatusEnum, StatusEnum as TtyStatusEnum, } from 'teleport/lib/term/ttyPlayer'; -import EventProvider from 'teleport/lib/term/ttyPlayerEventProvider'; +import { getAccessToken, getHostName } from 'teleport/services/api'; -import { ProgressBarTty } from './ProgressBar'; +import ProgressBar from './ProgressBar'; import Xterm from './Xterm'; -export default function Player({ sid, clusterId }) { - const { tty } = useSshPlayer(clusterId, sid); - const { statusText, status } = tty; - const eventCount = tty.getEventCount(); - const isError = status === TtyStatusEnum.ERROR; - const isLoading = status === TtyStatusEnum.LOADING; +export default function Player({ sid, clusterId, durationMs }) { + const { tty, playerStatus, statusText, time } = useStreamingSshPlayer( + clusterId, + sid + ); + const isError = playerStatus === TtyStatusEnum.ERROR; + const isLoading = playerStatus === TtyStatusEnum.LOADING; + const isPlaying = playerStatus === TtyStatusEnum.PLAYING; if (isError) { return ( @@ -53,22 +56,31 @@ export default function Player({ sid, clusterId }) { ); } - if (!isLoading && eventCount === 0) { - return ( - - - Recording for this session is not available. - - - ); - } - return ( - {eventCount > 0 && } + { + tty.move(pos); + tty.resumeTimeUpdates(); + }} + toggle={() => { + isPlaying ? tty.stop() : tty.play(); + }} + /> ); } @@ -87,35 +99,45 @@ const StyledPlayer = styled.div` justify-content: space-between; `; -function useSshPlayer(clusterId: string, sid: string) { - const tty = React.useMemo(() => { - const prefixUrl = cfg.getSshPlaybackPrefixUrl({ clusterId, sid }); - return new TtyPlayer(new EventProvider({ url: prefixUrl })); - }, [sid, clusterId]); +function useStreamingSshPlayer(clusterId: string, sid: string) { + const [playerStatus, setPlayerStatus] = React.useState(StatusEnum.LOADING); + const [statusText, setStatusText] = React.useState(''); + const [time, setTime] = React.useState(0); - // to trigger re-render when tty state changes - const [, rerender] = React.useState(tty.status); + const tty = React.useMemo(() => { + const url = cfg.api.ttyPlaybackWsAddr + .replace(':fqdn', getHostName()) + .replace(':clusterId', clusterId) + .replace(':sid', sid) + .replace(':token', getAccessToken()); + return new TtyPlayer({ url, setPlayerStatus, setStatusText, setTime }); + }, [clusterId, sid, setPlayerStatus, setStatusText, setTime]); React.useEffect(() => { - function onChange() { - // trigger rerender when status changes - rerender(tty.status); - } + tty.connect(); + tty.play(); - function cleanup() { + return () => { tty.stop(); tty.removeAllListeners(); - } - - tty.on('change', onChange); - tty.connect().then(() => { - tty.play(); - }); - - return cleanup; + }; }, [tty]); - return { - tty, - }; + return { tty, playerStatus, statusText, time }; +} + +function formatDisplayTime(ms: number) { + if (ms <= 0) { + return '00:00'; + } + + const totalSec = Math.floor(ms / 1000); + const totalDays = (totalSec % 31536000) % 86400; + const h = Math.floor(totalDays / 3600); + const m = Math.floor((totalDays % 3600) / 60); + const s = (totalDays % 3600) % 60; + + return `${h > 0 ? h + ':' : ''}${m.toString().padStart(2, '0')}:${s + .toString() + .padStart(2, '0')}`; } diff --git a/web/packages/teleport/src/Recordings/RecordingsList.tsx b/web/packages/teleport/src/Recordings/RecordingsList.tsx index 076fe271423..63dc816c661 100644 --- a/web/packages/teleport/src/Recordings/RecordingsList.tsx +++ b/web/packages/teleport/src/Recordings/RecordingsList.tsx @@ -134,7 +134,7 @@ const renderPlayCell = ( { clusterId, sid }, { recordingType, - durationMs: recordingType === 'desktop' ? duration : undefined, + durationMs: duration, } ); return ( diff --git a/web/packages/teleport/src/Recordings/__snapshots__/Recordings.story.test.tsx.snap b/web/packages/teleport/src/Recordings/__snapshots__/Recordings.story.test.tsx.snap index b925d40dbae..d0e35d6403e 100644 --- a/web/packages/teleport/src/Recordings/__snapshots__/Recordings.story.test.tsx.snap +++ b/web/packages/teleport/src/Recordings/__snapshots__/Recordings.story.test.tsx.snap @@ -915,7 +915,7 @@ exports[`rendering of Session Recordings 1`] = ` > . - */ - -/*eslint no-useless-escape: "off"*/ - -module.exports = { - events: [ - { - 'addr.local': '127.0.0.1:3022', - 'addr.remote': '127.0.0.1:51942', - event: 'session.start', - id: 0, - login: 'akontsevoy', - ms: -64, - offset: 0, - server_id: 'd1d92452-06b8-4828-abad-c1fb7ef947b3', - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '0:0', - time: '2016-05-09T14:57:05.936Z', - user: 'akontsevoy', - }, - { - event: 'resize', - id: 1, - login: 'akontsevoy', - ms: -59, - offset: 0, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '147:20', - time: '2016-05-09T14:57:05.941Z', - user: 'akontsevoy', - }, - { - bytes: 42, - event: 'print', - id: 2, - ms: 87, - offset: 0, - time: '2016-05-09T14:57:06.087Z', - }, - { - bytes: 1, - event: 'print', - id: 3, - ms: 1332, - offset: 42, - time: '2016-05-09T14:57:07.332Z', - }, - { - bytes: 1, - event: 'print', - id: 4, - ms: 1409, - offset: 43, - time: '2016-05-09T14:57:07.409Z', - }, - { - bytes: 2, - event: 'print', - id: 5, - ms: 1670, - offset: 44, - time: '2016-05-09T14:57:07.67Z', - }, - { - bytes: 69, - event: 'print', - id: 6, - ms: 1675, - offset: 46, - time: '2016-05-09T14:57:07.675Z', - }, - { - bytes: 8, - event: 'print', - id: 7, - ms: 1676, - offset: 115, - time: '2016-05-09T14:57:07.676Z', - }, - { - bytes: 32, - event: 'print', - id: 8, - ms: 1769, - offset: 123, - time: '2016-05-09T14:57:07.769Z', - }, - { - bytes: 86, - event: 'print', - id: 9, - ms: 1770, - offset: 155, - time: '2016-05-09T14:57:07.77Z', - }, - { - bytes: 104, - event: 'print', - id: 10, - ms: 1770, - offset: 241, - time: '2016-05-09T14:57:07.77Z', - }, - { - bytes: 99, - event: 'print', - id: 11, - ms: 1771, - offset: 345, - time: '2016-05-09T14:57:07.771Z', - }, - { - bytes: 88, - event: 'print', - id: 12, - ms: 1772, - offset: 444, - time: '2016-05-09T14:57:07.772Z', - }, - { - bytes: 87, - event: 'print', - id: 13, - ms: 1773, - offset: 532, - time: '2016-05-09T14:57:07.773Z', - }, - { - bytes: 4095, - event: 'print', - id: 14, - ms: 1774, - offset: 619, - time: '2016-05-09T14:57:07.774Z', - }, - { - bytes: 1906, - event: 'print', - id: 15, - ms: 1775, - offset: 4714, - time: '2016-05-09T14:57:07.775Z', - }, - { - bytes: 8, - event: 'print', - id: 16, - ms: 1942, - offset: 6620, - time: '2016-05-09T14:57:07.942Z', - }, - { - bytes: 96, - event: 'print', - id: 17, - ms: 1943, - offset: 6628, - time: '2016-05-09T14:57:07.943Z', - }, - { - bytes: 4095, - event: 'print', - id: 18, - ms: 1944, - offset: 6724, - time: '2016-05-09T14:57:07.944Z', - }, - { - bytes: 2080, - event: 'print', - id: 19, - ms: 1945, - offset: 10819, - time: '2016-05-09T14:57:07.945Z', - }, - { - event: 'resize', - id: 20, - login: 'akontsevoy', - ms: 4013, - offset: 12899, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '146:29', - time: '2016-05-09T14:57:10.013Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 21, - ms: 4014, - offset: 12899, - time: '2016-05-09T14:57:10.014Z', - }, - { - bytes: 2227, - event: 'print', - id: 22, - ms: 4017, - offset: 12907, - time: '2016-05-09T14:57:10.017Z', - }, - { - bytes: 2048, - event: 'print', - id: 23, - ms: 4019, - offset: 15134, - time: '2016-05-09T14:57:10.019Z', - }, - { - bytes: 4095, - event: 'print', - id: 24, - ms: 4023, - offset: 17182, - time: '2016-05-09T14:57:10.023Z', - }, - { - bytes: 1037, - event: 'print', - id: 25, - ms: 4025, - offset: 21277, - time: '2016-05-09T14:57:10.025Z', - }, - { - event: 'resize', - id: 26, - login: 'akontsevoy', - ms: 6586, - offset: 22314, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '146:31', - time: '2016-05-09T14:57:12.586Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 27, - ms: 6587, - offset: 22314, - time: '2016-05-09T14:57:12.587Z', - }, - { - bytes: 179, - event: 'print', - id: 28, - ms: 6588, - offset: 22322, - time: '2016-05-09T14:57:12.588Z', - }, - { - bytes: 2048, - event: 'print', - id: 29, - ms: 6589, - offset: 22501, - time: '2016-05-09T14:57:12.589Z', - }, - { - bytes: 4095, - event: 'print', - id: 30, - ms: 6590, - offset: 24549, - time: '2016-05-09T14:57:12.59Z', - }, - { - bytes: 3783, - event: 'print', - id: 31, - ms: 6591, - offset: 28644, - time: '2016-05-09T14:57:12.591Z', - }, - { - event: 'resize', - id: 32, - login: 'akontsevoy', - ms: 8129, - offset: 32427, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '146:25', - time: '2016-05-09T14:57:14.129Z', - user: 'akontsevoy', - }, - { - bytes: 37, - event: 'print', - id: 33, - ms: 8130, - offset: 32427, - time: '2016-05-09T14:57:14.13Z', - }, - { - bytes: 149, - event: 'print', - id: 34, - ms: 8131, - offset: 32464, - time: '2016-05-09T14:57:14.131Z', - }, - { - bytes: 4095, - event: 'print', - id: 35, - ms: 8132, - offset: 32613, - time: '2016-05-09T14:57:14.132Z', - }, - { - bytes: 3737, - event: 'print', - id: 36, - ms: 8133, - offset: 36708, - time: '2016-05-09T14:57:14.133Z', - }, - { - event: 'resize', - id: 37, - login: 'akontsevoy', - ms: 15669, - offset: 40445, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '106:25', - time: '2016-05-09T14:57:21.669Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 38, - ms: 15669, - offset: 40445, - time: '2016-05-09T14:57:21.669Z', - }, - { - bytes: 171, - event: 'print', - id: 39, - ms: 15670, - offset: 40453, - time: '2016-05-09T14:57:21.67Z', - }, - { - bytes: 2048, - event: 'print', - id: 40, - ms: 15671, - offset: 40624, - time: '2016-05-09T14:57:21.671Z', - }, - { - bytes: 4095, - event: 'print', - id: 41, - ms: 15672, - offset: 42672, - time: '2016-05-09T14:57:21.672Z', - }, - { - bytes: 572, - event: 'print', - id: 42, - ms: 15673, - offset: 46767, - time: '2016-05-09T14:57:21.673Z', - }, - { - event: 'resize', - id: 43, - login: 'akontsevoy', - ms: 17085, - offset: 47339, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '76:25', - time: '2016-05-09T14:57:23.085Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 44, - ms: 17086, - offset: 47339, - time: '2016-05-09T14:57:23.086Z', - }, - { - bytes: 176, - event: 'print', - id: 45, - ms: 17087, - offset: 47347, - time: '2016-05-09T14:57:23.087Z', - }, - { - bytes: 4095, - event: 'print', - id: 46, - ms: 17088, - offset: 47523, - time: '2016-05-09T14:57:23.088Z', - }, - { - bytes: 1783, - event: 'print', - id: 47, - ms: 17093, - offset: 51618, - time: '2016-05-09T14:57:23.093Z', - }, - { - event: 'resize', - id: 48, - login: 'akontsevoy', - ms: 18051, - offset: 53401, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '76:25', - time: '2016-05-09T14:57:24.051Z', - user: 'akontsevoy', - }, - { - event: 'resize', - id: 49, - login: 'akontsevoy', - ms: 18532, - offset: 53401, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '150:25', - time: '2016-05-09T14:57:24.532Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 50, - ms: 18535, - offset: 53401, - time: '2016-05-09T14:57:24.535Z', - }, - { - bytes: 4095, - event: 'print', - id: 51, - ms: 18536, - offset: 53409, - time: '2016-05-09T14:57:24.536Z', - }, - { - bytes: 4021, - event: 'print', - id: 52, - ms: 18537, - offset: 57504, - time: '2016-05-09T14:57:24.537Z', - }, - { - bytes: 292, - event: 'print', - id: 53, - ms: 20280, - offset: 61525, - time: '2016-05-09T14:57:26.28Z', - }, - { - bytes: 25, - event: 'print', - id: 54, - ms: 20484, - offset: 61817, - time: '2016-05-09T14:57:26.484Z', - }, - { - bytes: 15, - event: 'print', - id: 55, - ms: 20929, - offset: 61842, - time: '2016-05-09T14:57:26.929Z', - }, - { - bytes: 15, - event: 'print', - id: 56, - ms: 21001, - offset: 61857, - time: '2016-05-09T14:57:27.001Z', - }, - { - bytes: 15, - event: 'print', - id: 57, - ms: 21118, - offset: 61872, - time: '2016-05-09T14:57:27.118Z', - }, - { - event: 'resize', - id: 58, - login: 'akontsevoy', - ms: 27097, - offset: 61887, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '150:33', - time: '2016-05-09T14:57:33.097Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 59, - ms: 27098, - offset: 61887, - time: '2016-05-09T14:57:33.098Z', - }, - { - bytes: 181, - event: 'print', - id: 60, - ms: 27099, - offset: 61895, - time: '2016-05-09T14:57:33.099Z', - }, - { - bytes: 2048, - event: 'print', - id: 61, - ms: 27101, - offset: 62076, - time: '2016-05-09T14:57:33.101Z', - }, - { - bytes: 2048, - event: 'print', - id: 62, - ms: 27101, - offset: 64124, - time: '2016-05-09T14:57:33.101Z', - }, - { - bytes: 4095, - event: 'print', - id: 63, - ms: 27103, - offset: 66172, - time: '2016-05-09T14:57:33.103Z', - }, - { - bytes: 2572, - event: 'print', - id: 64, - ms: 27104, - offset: 70267, - time: '2016-05-09T14:57:33.104Z', - }, - { - event: 'resize', - id: 65, - login: 'akontsevoy', - ms: 28237, - offset: 72839, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '150:24', - time: '2016-05-09T14:57:34.237Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 66, - ms: 28238, - offset: 72839, - time: '2016-05-09T14:57:34.238Z', - }, - { - bytes: 187, - event: 'print', - id: 67, - ms: 28239, - offset: 72847, - time: '2016-05-09T14:57:34.239Z', - }, - { - bytes: 2048, - event: 'print', - id: 68, - ms: 28240, - offset: 73034, - time: '2016-05-09T14:57:34.24Z', - }, - { - bytes: 4095, - event: 'print', - id: 69, - ms: 28241, - offset: 75082, - time: '2016-05-09T14:57:34.241Z', - }, - { - bytes: 1443, - event: 'print', - id: 70, - ms: 28242, - offset: 79177, - time: '2016-05-09T14:57:34.242Z', - }, - { - event: 'resize', - id: 71, - login: 'akontsevoy', - ms: 34805, - offset: 80620, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '86:24', - time: '2016-05-09T14:57:40.805Z', - user: 'akontsevoy', - }, - { - bytes: 37, - event: 'print', - id: 72, - ms: 34806, - offset: 80620, - time: '2016-05-09T14:57:40.806Z', - }, - { - bytes: 147, - event: 'print', - id: 73, - ms: 34807, - offset: 80657, - time: '2016-05-09T14:57:40.807Z', - }, - { - bytes: 2048, - event: 'print', - id: 74, - ms: 34810, - offset: 80804, - time: '2016-05-09T14:57:40.81Z', - }, - { - bytes: 3816, - event: 'print', - id: 75, - ms: 34811, - offset: 82852, - time: '2016-05-09T14:57:40.811Z', - }, - { - event: 'resize', - id: 76, - login: 'akontsevoy', - ms: 35761, - offset: 86668, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '46:24', - time: '2016-05-09T14:57:41.761Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 77, - ms: 35762, - offset: 86668, - time: '2016-05-09T14:57:41.762Z', - }, - { - bytes: 4095, - event: 'print', - id: 78, - ms: 35763, - offset: 86676, - time: '2016-05-09T14:57:41.763Z', - }, - { - bytes: 713, - event: 'print', - id: 79, - ms: 35764, - offset: 90771, - time: '2016-05-09T14:57:41.764Z', - }, - { - event: 'resize', - id: 80, - login: 'akontsevoy', - ms: 36260, - offset: 91484, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '46:24', - time: '2016-05-09T14:57:42.26Z', - user: 'akontsevoy', - }, - { - event: 'resize', - id: 81, - login: 'akontsevoy', - ms: 36408, - offset: 91484, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '60:24', - time: '2016-05-09T14:57:42.408Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 82, - ms: 36409, - offset: 91484, - time: '2016-05-09T14:57:42.409Z', - }, - { - bytes: 170, - event: 'print', - id: 83, - ms: 36410, - offset: 91492, - time: '2016-05-09T14:57:42.41Z', - }, - { - bytes: 4095, - event: 'print', - id: 84, - ms: 36412, - offset: 91662, - time: '2016-05-09T14:57:42.412Z', - }, - { - bytes: 1003, - event: 'print', - id: 85, - ms: 36414, - offset: 95757, - time: '2016-05-09T14:57:42.414Z', - }, - { - event: 'resize', - id: 86, - login: 'akontsevoy', - ms: 36760, - offset: 96760, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '139:24', - time: '2016-05-09T14:57:42.76Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 87, - ms: 36761, - offset: 96760, - time: '2016-05-09T14:57:42.761Z', - }, - { - bytes: 105, - event: 'print', - id: 88, - ms: 36762, - offset: 96768, - time: '2016-05-09T14:57:42.762Z', - }, - { - bytes: 2048, - event: 'print', - id: 89, - ms: 36763, - offset: 96873, - time: '2016-05-09T14:57:42.763Z', - }, - { - bytes: 4095, - event: 'print', - id: 90, - ms: 36764, - offset: 98921, - time: '2016-05-09T14:57:42.764Z', - }, - { - bytes: 1217, - event: 'print', - id: 91, - ms: 36764, - offset: 103016, - time: '2016-05-09T14:57:42.764Z', - }, - { - event: 'resize', - id: 92, - login: 'akontsevoy', - ms: 37417, - offset: 104233, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '147:24', - time: '2016-05-09T14:57:43.417Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 93, - ms: 37426, - offset: 104233, - time: '2016-05-09T14:57:43.426Z', - }, - { - bytes: 4095, - event: 'print', - id: 94, - ms: 37428, - offset: 104241, - time: '2016-05-09T14:57:43.428Z', - }, - { - bytes: 3585, - event: 'print', - id: 95, - ms: 37430, - offset: 108336, - time: '2016-05-09T14:57:43.43Z', - }, - { - event: 'resize', - id: 96, - login: 'akontsevoy', - ms: 37670, - offset: 111921, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '157:24', - time: '2016-05-09T14:57:43.67Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 97, - ms: 37671, - offset: 111921, - time: '2016-05-09T14:57:43.671Z', - }, - { - bytes: 105, - event: 'print', - id: 98, - ms: 37671, - offset: 111929, - time: '2016-05-09T14:57:43.671Z', - }, - { - bytes: 4095, - event: 'print', - id: 99, - ms: 37673, - offset: 112034, - time: '2016-05-09T14:57:43.673Z', - }, - { - bytes: 3749, - event: 'print', - id: 100, - ms: 37674, - offset: 116129, - time: '2016-05-09T14:57:43.674Z', - }, - { - event: 'resize', - id: 101, - login: 'akontsevoy', - ms: 38509, - offset: 119878, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '115:24', - time: '2016-05-09T14:57:44.509Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 102, - ms: 38511, - offset: 119878, - time: '2016-05-09T14:57:44.511Z', - }, - { - bytes: 4095, - event: 'print', - id: 103, - ms: 38512, - offset: 119886, - time: '2016-05-09T14:57:44.512Z', - }, - { - bytes: 2729, - event: 'print', - id: 104, - ms: 38513, - offset: 123981, - time: '2016-05-09T14:57:44.513Z', - }, - { - bytes: 245, - event: 'print', - id: 105, - ms: 42326, - offset: 126710, - time: '2016-05-09T14:57:48.326Z', - }, - { - bytes: 254, - event: 'print', - id: 106, - ms: 42421, - offset: 126955, - time: '2016-05-09T14:57:48.421Z', - }, - { - bytes: 254, - event: 'print', - id: 107, - ms: 42433, - offset: 127209, - time: '2016-05-09T14:57:48.433Z', - }, - { - bytes: 250, - event: 'print', - id: 108, - ms: 42451, - offset: 127463, - time: '2016-05-09T14:57:48.451Z', - }, - { - event: 'resize', - id: 109, - login: 'akontsevoy', - ms: 44256, - offset: 127713, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '115:34', - time: '2016-05-09T14:57:50.256Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 110, - ms: 44258, - offset: 127713, - time: '2016-05-09T14:57:50.258Z', - }, - { - bytes: 29, - event: 'print', - id: 111, - ms: 44259, - offset: 127721, - time: '2016-05-09T14:57:50.259Z', - }, - { - bytes: 149, - event: 'print', - id: 112, - ms: 44259, - offset: 127750, - time: '2016-05-09T14:57:50.259Z', - }, - { - bytes: 2048, - event: 'print', - id: 113, - ms: 44260, - offset: 127899, - time: '2016-05-09T14:57:50.26Z', - }, - { - bytes: 4095, - event: 'print', - id: 114, - ms: 44261, - offset: 129947, - time: '2016-05-09T14:57:50.261Z', - }, - { - bytes: 3681, - event: 'print', - id: 115, - ms: 44262, - offset: 134042, - time: '2016-05-09T14:57:50.262Z', - }, - { - event: 'resize', - id: 116, - login: 'akontsevoy', - ms: 45237, - offset: 137723, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - size: '115:23', - time: '2016-05-09T14:57:51.237Z', - user: 'akontsevoy', - }, - { - bytes: 8, - event: 'print', - id: 117, - ms: 45238, - offset: 137723, - time: '2016-05-09T14:57:51.238Z', - }, - { - bytes: 182, - event: 'print', - id: 118, - ms: 45239, - offset: 137731, - time: '2016-05-09T14:57:51.239Z', - }, - { - bytes: 2048, - event: 'print', - id: 119, - ms: 45240, - offset: 137913, - time: '2016-05-09T14:57:51.24Z', - }, - { - bytes: 4095, - event: 'print', - id: 120, - ms: 45242, - offset: 139961, - time: '2016-05-09T14:57:51.242Z', - }, - { - bytes: 183, - event: 'print', - id: 121, - ms: 45243, - offset: 144056, - time: '2016-05-09T14:57:51.243Z', - }, - { - event: 'session.leave', - id: 122, - ms: 46403, - offset: 144239, - server_id: 'd1d92452-06b8-4828-abad-c1fb7ef947b3', - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - time: '2016-05-09T14:57:52.403Z', - user: 'akontsevoy', - }, - { - event: 'session.end', - id: 123, - sid: '4bac8c61-15f6-11e6-a2e6-f0def19340e2', - time: '2016-05-09T15:00:52.403Z', - user: 'akontsevoy', - }, - ], - - data: `]0;akontsevoy@x220: ~akontsevoy@x220:~$ mc -[?1049h(B(B - -[?1049l7[?47h[?1001s[?1002h[?1006h[?2004h[?1049h[?1h=(B | / ┐ - \ ┐]0;mc [akontsevoy@x220]:~>Hint: To look at the output of a command in the viewer, use M-!]0;mc [akontsevoy@x220]:~>]0;mc [akontsevoy@x220]:~>]0;mc [akontsevoy@x220]:~> Left File Command Options Right -┌<─ ~ ────────────────────────────.[^]>┐┌<─ ~ ────────────────────────────.[^]> -│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. │UP--DIR│Oct 2 2015││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.git-cre~l-cache(B│ 4096(B│Mar 19 16:18(B││/.git-cre~l-cache(B│ 4096(B│Mar 19 16:18(B│├──────────────────────────────────────┤├──────────────────────────────────────┤│UP--DIR ││UP--DIR │└──────────────────── 116G/219G (52%) ─┘└──────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$[^] 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov 7(BMkdir  8(BDelete 9(BPullDn10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h= [^] Left File Command Options Right -┌<─ ~ ─────────────────────────────────────────────────────────────.[^]>┐┌<─ ~ ──────────────────────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. │UP--DIR│Oct 2 2015││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B│├───────────────────────────────────────────────────────────────────────┤├────────────────────────────────────────────────────────────────────────┤│UP--DIR ││UP--DIR │└───────────────────────────────────────────────────── 116G/219G (52%) ─┘└────────────────────────────────────────────────────── 116G/219G (52%) ─┘Hint: To look at the output of a command in the viewer, use M-! -akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: Do you want lynx-style navigation? Set it in the Configuration dialog. [^] Left File Command Options Right -┌<─ ~ ─────────────────────────────────────────────────────────────.[^]>┐┌<─ ~ ─────────────────────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. │UP--DIR│Oct 2 2015││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.gnome (B│ 4096(B│Apr 7 22:06(B││/.gnome (B│ 4096(B│Apr 7 22:06(B││/.gnupg (B│ 4096(B│Dec 14 14:23(B││/.gnupg (B│ 4096(B│Dec 14 14:23(B││/.gvfs (B│ 4096(B│Nov 2 2015(B││/.gvfs (B│ 4096(B│Nov 2 2015(B││/.gvm (B│ 4096(B│Nov 28 16:48(B││/.gvm (B│ 4096(B│Nov 28 16:48(B│├───────────────────────────────────────────────────────────────────────┤├───────────────────────────────────────────────────────────────────────┤│UP--DIR ││UP--DIR │└───────────────────────────────────────────────────── 116G/219G (52%) ─┘└───────────────────────────────────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: Selecting directories: add a slash to the end of the matching pattern. [^] Left File Command Options Right -┌<─ ~ ─────────────────────────────────────────────────────────────.[^]>┐┌<─ ~ ─────────────────────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. │UP--DIR│Oct 2 2015││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.gnome (B│ 4096(B│Apr 7 22:06(B││/.gnome (B│ 4096(B│Apr 7 22:06(B││/.gnupg (B│ 4096(B│Dec 14 14:23(B││/.gnupg (B│ 4096(B│Dec 14 14:23(B││/.gvfs (B│ 4096(B│Nov 2 2015(B││/.gvfs (B│ 4096(B│Nov 2 2015(B││/.gvm (B│ 4096(B│Nov 28 16:48(B││/.gvm (B│ 4096(B│Nov 28 16:48(B││/.hplip (B│ 4096(B│Mar 7 21:38(B││/.hplip (B│ 4096(B│Mar 7 21:38(B││/.lastpass (B│ 4096(B│Dec 14 16:01(B││/.lastpass (B│ 4096(B│Dec 14 16:01(B│├───────────────────────────────────────────────────────────────────────┤├───────────────────────────────────────────────────────────────────────┤│UP--DIR ││UP--DIR │└───────────────────────────────────────────────────── 116G/219G (52%) ─┘└───────────────────────────────────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: Find File: you can work on the files found using the Panelize button. [^] Left File Command Options Right -┌<─ ~ ─────────────────────────────────────────────────────────────.[^]>┐┌<─ ~ ─────────────────────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. │UP--DIR│Oct 2 2015││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B│├───────────────────────────────────────────────────────────────────────┤├───────────────────────────────────────────────────────────────────────┤│UP--DIR ││UP--DIR │└───────────────────────────────────────────────────── 116G/219G (52%) ─┘└───────────────────────────────────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: VFS coolness: tap enter on a tar file to examine its contents. [^] Left File Command Options Right -┌<─ ~ ─────────────────────────────────────────.[^]>┐┌<─ ~ ─────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. │UP--DIR│Oct 2 2015││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B│├───────────────────────────────────────────────────┤├───────────────────────────────────────────────────┤│UP--DIR ││UP--DIR │└───────────────────────────────── 116G/219G (52%) ─┘└───────────────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: Completion works on all input lines in all dialogs. Just press M-Tab. [^] Left File Command Options Right -┌<─ ~ ──────────────────────────.[^]>┐┌<─ ~ ──────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. │UP--DIR│Oct 2 2015││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.git-cr~-cache(B│ 4096(B│Mar 19 16:18(B││/.git-cr~-cache(B│ 4096(B│Mar 19 16:18(B│├────────────────────────────────────┤├────────────────────────────────────┤│UP--DIR ││UP--DIR │└────────────────── 116G/219G (52%) ─┘└────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRe~ov 7(BMkdir 8(BDelete 9(BPullDn10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: Want to do complex searches? Use the External Panelize command. [^] Left File Command Options Right -┌<─ ~ ───────────────────────────────────────────────────────────────.[^]>┐┌<─ ~ ───────────────────────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. │UP--DIR│Oct 2 2015││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B│├─────────────────────────────────────────────────────────────────────────┤├─────────────────────────────────────────────────────────────────────────┤│UP--DIR ││UP--DIR │└─────────────────────────────────────────────────────── 116G/219G (52%) ─┘└─────────────────────────────────────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit/.. (B│UP--DIR(B│Oct 2 2015(B/.gimp-2.8 │ 4096│Apr 25 22:55/.gimp-2.8gdfg7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: You can specify the username when doing ftps: 'cd ftp://user@machine.edu' gdfg[^] Left File Command Options Right -┌<─ ~ ───────────────────────────────────────────────────────────────.[^]>┐┌<─ ~ ───────────────────────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 │ 4096│Apr 25 22:55││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.gnome (B│ 4096(B│Apr 7 22:06(B││/.gnome (B│ 4096(B│Apr 7 22:06(B││/.gnupg (B│ 4096(B│Dec 14 14:23(B││/.gnupg (B│ 4096(B│Dec 14 14:23(B││/.gvfs (B│ 4096(B│Nov 2 2015(B││/.gvfs (B│ 4096(B│Nov 2 2015(B││/.gvm (B│ 4096(B│Nov 28 16:48(B││/.gvm (B│ 4096(B│Nov 28 16:48(B││/.hplip (B│ 4096(B│Mar 7 21:38(B││/.hplip (B│ 4096(B│Mar 7 21:38(B││/.lastpass (B│ 4096(B│Dec 14 16:01(B││/.lastpass (B│ 4096(B│Dec 14 16:01(B││/.local (B│ 4096(B│Apr 8 15:25(B││/.local (B│ 4096(B│Apr 8 15:25(B││/.macromedia (B│ 4096(B│Oct 2 2015(B││/.macromedia (B│ 4096(B│Oct 2 2015(B│├─────────────────────────────────────────────────────────────────────────┤├─────────────────────────────────────────────────────────────────────────┤│/.gimp-2.8 ││UP--DIR │└─────────────────────────────────────────────────────── 116G/219G (52%) ─┘└─────────────────────────────────────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: Find File: you can work on the files found using the Panelize button. gdfg[^] Left File Command Options Right -┌<─ ~ ───────────────────────────────────────────────────────────────.[^]>┐┌<─ ~ ───────────────────────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 │ 4096│Apr 25 22:55││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B│├─────────────────────────────────────────────────────────────────────────┤├─────────────────────────────────────────────────────────────────────────┤│/.gimp-2.8 ││UP--DIR │└─────────────────────────────────────────────────────── 116G/219G (52%) ─┘└─────────────────────────────────────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: Need to quote a character? Use Control-q and the character. gdfg[^] Left File Command Options Right -┌<─ ~ ───────────────────────────────.[^]>┐┌<─ ~ ───────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 │ 4096│Apr 25 22:55││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B│├─────────────────────────────────────────┤├─────────────────────────────────────────┤│/.gimp-2.8 ││UP--DIR │└─────────────────────── 116G/219G (52%) ─┘└─────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov 7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: You may specify the external viewer with gdfg[^] Left File Command Options Right -┌<─ ~ ───────────.[^]>┐┌<─ ~ ───────────.[^]>┐│.nName (B│Siz(B│Modify t(B││.nName (B│Siz(B│Modify t(B││/.. (B│DIR(B│ 2 2015(B││/.. (B│DIR(B│ 2 2015(B││/.Skype (B│ 4K(B│ 9 10:51(B││/.Skype (B│ 4K(B│ 9 10:51(B││/.adobe (B│ 4K(B│ 2 2015(B││/.adobe (B│ 4K(B│ 2 2015(B││/.an~ble(B│ 4K(B│24 20:35(B││/.an~ble(B│ 4K(B│24 20:35(B││/.ap~ude(B│ 4K(B│28 14:51(B││/.ap~ude(B│ 4K(B│28 14:51(B││/.atom (B│ 4K(B│ 4 2015(B││/.atom (B│ 4K(B│ 4 2015(B││/.aws (B│ 4K(B│ 8 16:09(B││/.aws (B│ 4K(B│ 8 16:09(B││/.cache (B│ 4K(B│ 7 15:56(B││/.cache (B│ 4K(B│ 7 15:56(B││/.config(B│ 4K(B│ 7 22:22(B││/.config(B│ 4K(B│ 7 22:22(B││/.dbus (B│ 4K(B│ 2 2015(B││/.dbus (B│ 4K(B│ 2 2015(B││/.dlv (B│ 4K(B│10 14:15(B││/.dlv (B│ 4K(B│10 14:15(B││/.em~s.d(B│ 4K(B│20 09:29(B││/.em~s.d(B│ 4K(B│20 09:29(B││/.fo~ver(B│ 4K(B│20 15:29(B││/.fo~ver(B│ 4K(B│20 15:29(B││/.gconf (B│ 4K(B│ 7 22:22(B││/.gconf (B│ 4K(B│ 7 22:22(B││/.gi~2.8│ 4K│25 22:55││/.gi~2.8(B│ 4K(B│25 22:55(B│├─────────────────────┤├─────────────────────┤│/.gimp-2.8 ││UP--DIR │└─── 116G/219G (52%) ─┘└─── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRe~ov7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: Are some of your keys not working? Look at Options/Lea gdfg[^] Left File Command Options Right -┌<─ ~ ──────────────────.[^]>┐┌<─ ~ ──────────────────.[^]>┐│.n Name (B│Size (B│Modify tim(B││.n Name (B│Size (B│Modify tim(B││/.. (B│--DIR(B│t 2 2015(B││/.. (B│--DIR(B│t 2 2015(B││/.Skype (B│ 4096(B│y 9 10:51(B││/.Skype (B│ 4096(B│y 9 10:51(B││/.adobe (B│ 4096(B│t 2 2015(B││/.adobe (B│ 4096(B│t 2 2015(B││/.ansible (B│ 4096(B│r 24 20:35(B││/.ansible (B│ 4096(B│r 24 20:35(B││/.aptitude (B│ 4096(B│r 28 14:51(B││/.aptitude (B│ 4096(B│r 28 14:51(B││/.atom (B│ 4096(B│v 4 2015(B││/.atom (B│ 4096(B│v 4 2015(B││/.aws (B│ 4096(B│r 8 16:09(B││/.aws (B│ 4096(B│r 8 16:09(B││/.cache (B│ 4096(B│y 7 15:56(B││/.cache (B│ 4096(B│y 7 15:56(B││/.config (B│ 4096(B│y 7 22:22(B││/.config (B│ 4096(B│y 7 22:22(B││/.dbus (B│ 4096(B│v 2 2015(B││/.dbus (B│ 4096(B│v 2 2015(B││/.dlv (B│ 4096(B│n 10 14:15(B││/.dlv (B│ 4096(B│n 10 14:15(B││/.emacs.d (B│ 4096(B│n 20 09:29(B││/.emacs.d (B│ 4096(B│n 20 09:29(B││/.forever (B│ 4096(B│n 20 15:29(B││/.forever (B│ 4096(B│n 20 15:29(B││/.gconf (B│ 4096(B│y 7 22:22(B││/.gconf (B│ 4096(B│y 7 22:22(B││/.gimp-2.8 │ 4096│r 25 22:55││/.gimp-2.8 (B│ 4096(B│r 25 22:55(B│├────────────────────────────┤├────────────────────────────┤│/.gimp-2.8 ││UP--DIR │└────────── 116G/219G (52%) ─┘└────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRe~ov 7(BMkdir 8(BDe~te7[?47h]0;mc [akontsevoy@x220]:~>[?1h= gdfg[^] Left File Command Options Right -┌<─ ~ ─────────────────────────────────────────────────────────.[^]>┐┌<─ ~ ──────────────────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 │ 4096│Apr 25 22:55││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B│├───────────────────────────────────────────────────────────────────┤├────────────────────────────────────────────────────────────────────┤│/.gimp-2.8 ││UP--DIR │└───────────────────────────────────────────────── 116G/219G (52%) ─┘└────────────────────────────────────────────────── 116G/219G (52%) ─┘Hint: Are some of your keys not working? Look at Options/Learn keys. -akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h= gdfg[^] Left File Command Options Right -┌<─ ~ ─────────────────────────────────────────────────────────────.[^]>┐┌<─ ~ ──────────────────────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 │ 4096│Apr 25 22:55││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B│├───────────────────────────────────────────────────────────────────────┤├────────────────────────────────────────────────────────────────────────┤│/.gimp-2.8 ││UP--DIR │└───────────────────────────────────────────────────── 116G/219G (52%) ─┘└────────────────────────────────────────────────────── 116G/219G (52%) ─┘Hint: Are some of your keys not working? Look at Options/Learn keys. -akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h= gdfg[^] Left File Command Options Right -┌<─ ~ ──────────────────────────────────────────────────────────────────.[^]>┐┌<─ ~ ───────────────────────────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 │ 4096│Apr 25 22:55││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B│├────────────────────────────────────────────────────────────────────────────┤├─────────────────────────────────────────────────────────────────────────────┤│/.gimp-2.8 ││UP--DIR │└────────────────────────────────────────────────────────── 116G/219G (52%) ─┘└─────────────────────────────────────────────────────────── 116G/219G (52%) ─┘Hint: Are some of your keys not working? Look at Options/Learn keys. -akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: Leap to frequently used directories in a single bound with C-\. gdfg[^] Left File Command Options Right -┌<─ ~ ─────────────────────────────────────────────.[^]>┐┌<─ ~ ──────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 │ 4096│Apr 25 22:55││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B│├───────────────────────────────────────────────────────┤├────────────────────────────────────────────────────────┤│/.gimp-2.8 ││UP--DIR │└───────────────────────────────────── 116G/219G (52%) ─┘└────────────────────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit/.forever │ 4096│Jan 20 15:29/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(Bforever /.emacs.d │ 4096│Jan 20 09:29/.forever (B│ 4096(B│Jan 20 15:29(Bemacs.d/.dlv │ 4096│Jan 10 14:15/.emacs.d (B│ 4096(B│Jan 20 09:29(Bdlv /.dbus │ 4096│Nov 2 2015/.dlv (B│ 4096(B│Jan 10 14:15(Bbus7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: To mark directories on the select dialog box, append a slash. gdfg[^] Left File Command Options Right -┌<─ ~ ─────────────────────────────────────────────.[^]>┐┌<─ ~ ──────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus │ 4096│Nov 2 2015││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.gimp-2.8 (B│ 4096(B│Apr 25 22:55(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.git-credential-cache (B│ 4096(B│Mar 19 16:18(B││/.gnome (B│ 4096(B│Apr 7 22:06(B││/.gnome (B│ 4096(B│Apr 7 22:06(B││/.gnupg (B│ 4096(B│Dec 14 14:23(B││/.gnupg (B│ 4096(B│Dec 14 14:23(B││/.gvfs (B│ 4096(B│Nov 2 2015(B││/.gvfs (B│ 4096(B│Nov 2 2015(B││/.gvm (B│ 4096(B│Nov 28 16:48(B││/.gvm (B│ 4096(B│Nov 28 16:48(B││/.hplip (B│ 4096(B│Mar 7 21:38(B││/.hplip (B│ 4096(B│Mar 7 21:38(B││/.lastpass (B│ 4096(B│Dec 14 16:01(B││/.lastpass (B│ 4096(B│Dec 14 16:01(B││/.local (B│ 4096(B│Apr 8 15:25(B││/.local (B│ 4096(B│Apr 8 15:25(B││/.macromedia (B│ 4096(B│Oct 2 2015(B││/.macromedia (B│ 4096(B│Oct 2 2015(B││/.mozilla (B│ 4096(B│Oct 2 2015(B││/.mozilla (B│ 4096(B│Oct 2 2015(B│├───────────────────────────────────────────────────────┤├────────────────────────────────────────────────────────┤│/.dbus ││UP--DIR │└───────────────────────────────────── 116G/219G (52%) ─┘└────────────────────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit7[?47h]0;mc [akontsevoy@x220]:~>[?1h=Hint: You may specify the editor for F4 with the shell variable EDITOR. gdfg[^] Left File Command Options Right -┌<─ ~ ─────────────────────────────────────────────.[^]>┐┌<─ ~ ──────────────────────────────────────────────.[^]>┐│.n Name (B│ Size (B│Modify time (B││.n Name (B│ Size (B│Modify time (B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.. (B│UP--DIR(B│Oct 2 2015(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.Skype (B│ 4096(B│May 9 10:51(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.adobe (B│ 4096(B│Oct 2 2015(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.ansible (B│ 4096(B│Mar 24 20:35(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.aptitude (B│ 4096(B│Apr 28 14:51(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.atom (B│ 4096(B│Nov 4 2015(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.aws (B│ 4096(B│Apr 8 16:09(B││/.cache (B│ 4096(B│May 7 15:56(B││/.cache (B│ 4096(B│May 7 15:56(B││/.config (B│ 4096(B│May 7 22:22(B││/.config (B│ 4096(B│May 7 22:22(B││/.dbus │ 4096│Nov 2 2015││/.dbus (B│ 4096(B│Nov 2 2015(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.dlv (B│ 4096(B│Jan 10 14:15(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.emacs.d (B│ 4096(B│Jan 20 09:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.forever (B│ 4096(B│Jan 20 15:29(B││/.gconf (B│ 4096(B│May 7 22:22(B││/.gconf (B│ 4096(B│May 7 22:22(B│├───────────────────────────────────────────────────────┤├────────────────────────────────────────────────────────┤│/.dbus ││UP--DIR │└───────────────────────────────────── 116G/219G (52%) ─┘└────────────────────────────────────── 116G/219G (52%) ─┘akontsevoy@x220:~$ - 1(BHelp  2(BMenu  3(BView  4(BEdit  5(BCopy  6(BRenMov  7(BMkdir  8(BDelete  9(BPullDn 10(BQuit`, -}; diff --git a/web/packages/teleport/src/lib/term/terminal.ts b/web/packages/teleport/src/lib/term/terminal.ts index 6ce0d17bf14..77fb4c28acb 100644 --- a/web/packages/teleport/src/lib/term/terminal.ts +++ b/web/packages/teleport/src/lib/term/terminal.ts @@ -158,7 +158,11 @@ export default class TtyTerminal { _processData(data) { try { this.tty.pauseFlow(); - this.term.write(data, () => this.tty.resumeFlow()); + + // during a live session, data is emitted as a string. + // during playback, data from the websocket comes over as a DataView + const d: any = typeof data === 'string' ? data : new Uint8Array(data); + this.term.write(d, () => this.tty.resumeFlow()); } catch (err) { logger.error('xterm.write', data, err); // recover xtermjs by resetting it diff --git a/web/packages/teleport/src/lib/term/tty.ts b/web/packages/teleport/src/lib/term/tty.ts index 98f8257073a..88a18bcc5d2 100644 --- a/web/packages/teleport/src/lib/term/tty.ts +++ b/web/packages/teleport/src/lib/term/tty.ts @@ -181,6 +181,7 @@ class Tty extends EventEmitterWebAuthnSender { try { const uintArray = new Uint8Array(ev.data); const msg = this._proto.decode(uintArray); + switch (msg.type) { case MessageTypeEnum.WEBAUTHN_CHALLENGE: this.emit(TermEvent.WEBAUTHN_CHALLENGE, msg.payload); diff --git a/web/packages/teleport/src/lib/term/ttyAddressResolver.js b/web/packages/teleport/src/lib/term/ttyAddressResolver.js index 59abdfcf02f..efd04dc28a2 100644 --- a/web/packages/teleport/src/lib/term/ttyAddressResolver.js +++ b/web/packages/teleport/src/lib/term/ttyAddressResolver.js @@ -16,7 +16,11 @@ * along with this program. If not, see . */ -export default class AddressResolver { +/** + * LiveSessionAddressResolver is an address resolver that computes + * a URL to start a new web-based SSH session. + */ +export default class LiveSessionAddressResolver { _cfg = { ttyUrl: null, ttyParams: {}, diff --git a/web/packages/teleport/src/lib/term/ttyPlayer.js b/web/packages/teleport/src/lib/term/ttyPlayer.js index ee5d2b96b6d..3b33b33c65d 100644 --- a/web/packages/teleport/src/lib/term/ttyPlayer.js +++ b/web/packages/teleport/src/lib/term/ttyPlayer.js @@ -16,263 +16,235 @@ * along with this program. If not, see . */ -import BufferModule from 'buffer/'; +import { throttle } from 'shared/utils/highbar'; import Logger from 'shared/libs/logger'; import Tty from './tty'; -import { TermEvent } from './enums'; -import { onlyPrintEvents } from './ttyPlayerEventProvider'; +import { TermEvent, WebsocketCloseCode } from './enums'; const logger = Logger.create('TtyPlayer'); -const STREAM_START_INDEX = 0; -const PLAY_SPEED = 10; -export const Buffer = BufferModule.Buffer; export const StatusEnum = { PLAYING: 'PLAYING', ERROR: 'ERROR', PAUSED: 'PAUSED', LOADING: 'LOADING', + COMPLETE: 'COMPLETE', }; +const messageTypePty = 1; +const messageTypeError = 2; +const messageTypePlayPause = 3; +const messageTypeSeek = 4; +const messageTypeResize = 5; + +const actionPlay = 0; +const actionPause = 1; + +// we update the time every time we receive data, or +// at this interval (which ensures that the progress +// bar updates even when we aren't receiving data) +const PROGRESS_UPDATE_INTERVAL_MS = 50; + export default class TtyPlayer extends Tty { - constructor(eventProvider) { + constructor({ url, setPlayerStatus, setStatusText, setTime }) { super({}); - this.currentEventIndex = 0; - this.current = 0; - this.duration = 0; - this.status = StatusEnum.LOADING; - this.statusText = ''; - this._posToEventIndexMap = []; - this._eventProvider = eventProvider; + this._url = url; + this._setPlayerStatus = setPlayerStatus; + this._setStatusText = setStatusText; - // _chunkQueue is a list of data chunks waiting to be rendered by the term. - this._chunkQueue = []; - // _writeInFlight prevents sending more data to xterm while a prior render has not finished yet. - this._writeInFlight = false; + this._paused = false; + this._lastPlayedTimestamp = 0; + + this._sendTimeUpdates = true; + this._setTime = throttle(t => setTime(t), PROGRESS_UPDATE_INTERVAL_MS); + this._lastUpdate = 0; + this._timeout = null; + } + + // Override the base class connection, which uses the envelope-based + // websocket protocol (this protocol doesn't support sending timing data). + connect() { + this._setPlayerStatus(StatusEnum.LOADING); + + this.webSocket = new WebSocket(this._url); + this.webSocket.binaryType = 'arraybuffer'; + this.webSocket.onopen = () => this.emit('open'); + this.webSocket.onmessage = m => this.onMessage(m); + this.webSocket.onclose = e => { + logger.debug('websocket closed', e); + this.cancelTimeUpdate(); + + this.webSocket.close(); + this.webSocket.onopen = null; + this.webSocket.onclose = null; + this.webSocket.onmessage = null; + this.webSocket = null; + + this.emit(TermEvent.CONN_CLOSE, e); + this._setPlayerStatus(StatusEnum.COMPLETE); + }; + } + + suspendTimeUpdates() { + this._sendTimeUpdates = false; + } + + resumeTimeUpdates() { + this._sendTimeUpdates = true; + } + + setTime(t) { + // time updates are suspended when a user is dragging the slider to + // a new position (it's very disruptive if we're updating the slider + // position every few milliseconds while the user is trying to + // reposition it) + if (this._sendTimeUpdates) { + this._setTime(t); + } + } + + disconnect(closeCode = WebsocketCloseCode.NORMAL) { + this.cancelTimeUpdate(); + if (this.webSocket !== null) { + this.webSocket.close(closeCode); + } + } + + scheduleNextUpdate(current) { + this._timeout = setTimeout(() => { + const delta = Date.now() - this._lastUpdate; + const next = current + delta; + this.setTime(next); + this._lastUpdate = Date.now(); + + this.scheduleNextUpdate(next); + }, PROGRESS_UPDATE_INTERVAL_MS); + } + + cancelTimeUpdate() { + if (this._timeout != null) { + clearTimeout(this._timeout); + this._timeout = null; + } + } + + onMessage(m) { + try { + const dv = new DataView(m.data); + const typ = dv.getUint8(0); + const len = dv.getUint16(1); + + // see lib/web/tty_playback.go for details on this protocol + switch (typ) { + case messageTypePty: + this.cancelTimeUpdate(); + + const delay = Number(dv.getBigUint64(3)); + const data = dv.buffer.slice( + dv.byteOffset + 11, + dv.byteOffset + 11 + len + ); + + this.emit(TermEvent.DATA, data); + this._lastPlayedTimestamp = delay; + + this._lastUpdate = Date.now(); + this.setTime(delay); + + // schedule the next time update (in case this + // part of the recording is dead time) + // TODO(zmb3): implement this for desktops too + if (!this._paused) { + this.scheduleNextUpdate(delay); + } + break; + + case messageTypeError: + // ignore the severity byte at index 3 (we display all errors identically) + const msgLen = dv.getUint16(4); + const msg = new TextDecoder().decode( + dv.buffer.slice(dv.byteOffset + 6, dv.byteOffset + 6 + msgLen) + ); + this._setStatusText(msg); + this._setPlayerStatus(StatusEnum.ERROR); + this.disconnect(); + return; + + case messageTypeResize: + const w = dv.getUint16(3); + const h = dv.getUint16(5); + this.emit(TermEvent.RESIZE, { w, h }); + return; + + default: + logger.warn('unexpected message type', typ); + return; + } + } catch (err) { + logger.error('failed to parse incoming message', err); + } } // override send() {} - - // override - connect() { - this.status = StatusEnum.LOADING; - this._change(); - return this._eventProvider - .init() - .then(() => { - this._init(); - this.status = StatusEnum.PAUSED; - }) - .catch(err => { - logger.error('unable to init event provider', err); - this._handleError(err); - }) - .finally(this._change.bind(this)); - } - - pauseFlow() { - this._writeInFlight = true; - } - - resumeFlow() { - this._writeInFlight = false; - this._chunkDequeue(); - } + pauseFlow() {} + resumeFlow() {} move(newPos) { - if (!this.isReady()) { - return; - } - - if (newPos === undefined) { - newPos = this.current + 1; - } - - if (newPos < 0) { - newPos = 0; - } - - if (newPos > this.duration) { - this.stop(); - } - - const newEventIndex = this._getEventIndex(newPos) + 1; - - if (newEventIndex === this.currentEventIndex) { - this.current = newPos; - this._change(); - return; - } - - const isRewind = this.currentEventIndex > newEventIndex; + this.cancelTimeUpdate(); try { - // we cannot playback the content within terminal so instead: - // 1. tell terminal to reset. - // 2. tell terminal to render 1 huge chunk that has everything up to current - // location. - if (isRewind) { - this._chunkQueue = []; - this.emit(TermEvent.RESET); - } + const buffer = new ArrayBuffer(11); + const dv = new DataView(buffer); + dv.setUint8(0, messageTypeSeek); + dv.setUint16(1, 8 /* length */); + dv.setBigUint64(3, BigInt(newPos)); + this.webSocket.send(dv); + } catch (e) { + logger.error('error seeking', e); + } - const from = isRewind ? 0 : this.currentEventIndex; - const to = newEventIndex; - const events = this._eventProvider.events.slice(from, to); - const printEvents = events.filter(onlyPrintEvents); - - this._render(printEvents); - this.currentEventIndex = newEventIndex; - this.current = newPos; - this._change(); - } catch (err) { - logger.error('move', err); - this._handleError(err); + if (newPos < this._lastPlayedTimestamp) { + this.emit(TermEvent.RESET); + } else if (this._paused) { + // if we're paused, we want the scrubber to "stick" at the new + // time until we press play (rather than waiting for us to click + // play and start receiving new data) + this._setTime(newPos); } } stop() { - this.status = StatusEnum.PAUSED; - this.timer = clearInterval(this.timer); - this._change(); + this._paused = true; + this.cancelTimeUpdate(); + this._setPlayerStatus(StatusEnum.PAUSED); + + const buffer = new ArrayBuffer(4); + const dv = new DataView(buffer); + dv.setUint8(0, messageTypePlayPause); + dv.setUint16(1, 1 /* size */); + dv.setUint8(3, actionPause); + this.webSocket.send(dv); } play() { - if (this.status === StatusEnum.PLAYING) { + this._paused = false; + this._setPlayerStatus(StatusEnum.PLAYING); + + // the very first play call happens before we've even + // connected - we only need to send the websocket message + // for subsequent calls + if (this.webSocket.readyState !== WebSocket.OPEN) { return; } - this.status = StatusEnum.PLAYING; - // start from the beginning if reached the end of the session - if (this.current >= this.duration) { - this.current = STREAM_START_INDEX; - this.emit(TermEvent.RESET); - } - - this.timer = setInterval(this.move.bind(this), PLAY_SPEED); - this._change(); - } - - getCurrentTime() { - if (this.currentEventIndex) { - let { displayTime } = - this._eventProvider.events[this.currentEventIndex - 1]; - return displayTime; - } else { - return '--:--'; - } - } - - getEventCount() { - return this._eventProvider.events.length; - } - - isLoading() { - return this.status === StatusEnum.LOADING; - } - - isPlaying() { - return this.status === StatusEnum.PLAYING; - } - - isError() { - return this.status === StatusEnum.ERROR; - } - - isReady() { - return ( - this.status !== StatusEnum.LOADING && this.status !== StatusEnum.ERROR - ); - } - - disconnect() { - // do nothing - } - - _init() { - this.duration = this._eventProvider.getDuration(); - this._eventProvider.events.forEach(item => - this._posToEventIndexMap.push(item.msNormalized) - ); - } - - _chunkDequeue() { - const chunk = this._chunkQueue.shift(); - if (!chunk) { - return; - } - - const str = chunk.data.join(''); - this.emit(TermEvent.RESIZE, { h: chunk.h, w: chunk.w }); - this.emit(TermEvent.DATA, str); - } - - _render(events) { - if (!events || events.length === 0) { - return; - } - - const groups = [ - { - data: [events[0].data], - w: events[0].w, - h: events[0].h, - }, - ]; - - let cur = groups[0]; - - // group events by screen size and construct 1 chunk of data per group - for (let i = 1; i < events.length; i++) { - if (cur.w === events[i].w && cur.h === events[i].h) { - cur.data.push(events[i].data); - } else { - cur = { - data: [events[i].data], - w: events[i].w, - h: events[i].h, - }; - - groups.push(cur); - } - } - - this._chunkQueue = [...this._chunkQueue, ...groups]; - if (!this._writeInFlight) { - this._chunkDequeue(); - } - } - - _getEventIndex(num) { - const arr = this._posToEventIndexMap; - var low = 0; - var hi = arr.length - 1; - - while (hi - low > 1) { - const mid = Math.floor((low + hi) / 2); - if (arr[mid] < num) { - low = mid; - } else { - hi = mid; - } - } - - if (num - arr[low] <= arr[hi] - num) { - return low; - } - - return hi; - } - - _change() { - this.emit('change'); - } - - _handleError(err) { - this.status = StatusEnum.ERROR; - this.statusText = err.message; + const buffer = new ArrayBuffer(4); + const dv = new DataView(buffer); + dv.setUint8(0, messageTypePlayPause); + dv.setUint16(1, 1 /* size */); + dv.setUint8(3, actionPlay); + this.webSocket.send(dv); } } diff --git a/web/packages/teleport/src/lib/term/ttyPlayer.test.js b/web/packages/teleport/src/lib/term/ttyPlayer.test.js index b2de31641c3..569d3f3e3b0 100644 --- a/web/packages/teleport/src/lib/term/ttyPlayer.test.js +++ b/web/packages/teleport/src/lib/term/ttyPlayer.test.js @@ -17,247 +17,127 @@ */ import '@gravitational/shared/libs/polyfillFinally'; -import api from 'teleport/services/api'; + +import WS from 'jest-websocket-mock'; import { TermEvent } from 'teleport/lib/term/enums'; -import TtyPlayer, { Buffer } from './ttyPlayer'; -import EventProvider, { MAX_SIZE } from './ttyPlayerEventProvider'; -import sample from './fixtures/streamData'; - -describe('lib/term/ttyPlayer/eventProvider', () => { - afterEach(function () { - jest.clearAllMocks(); - }); - - it('should create an instance', () => { - const provider = new EventProvider({ url: 'sample.com' }); - expect(provider.events).toEqual([]); - }); - - it('should load events and initialize itself', async () => { - const provider = new EventProvider({ url: 'sample.com' }); - - jest.spyOn(api, 'get').mockImplementation(() => Promise.resolve(sample)); - jest.spyOn(provider, '_createEvents'); - jest.spyOn(provider, '_normalizeEventsByTime'); - jest - .spyOn(provider, '_fetchContent') - .mockImplementation(() => Promise.resolve()); - jest.spyOn(provider, '_populatePrintEvents').mockImplementation(); - - await provider.init(); - - expect(api.get).toHaveBeenCalledWith('sample.com/events'); - expect(provider._createEvents).toHaveBeenCalledWith(sample.events); - expect(provider._normalizeEventsByTime).toHaveBeenCalled(); - expect(provider._fetchContent).toHaveBeenCalled(); - expect(provider._populatePrintEvents).toHaveBeenCalled(); - }); - - it('should create event objects', () => { - const provider = new EventProvider({ url: 'sample.com' }); - const events = provider._createEvents(sample.events); - const eventObj = { - eventType: 'print', - displayTime: '00:45', - ms: 4523, - bytes: 6516, - offset: 137723, - data: null, - w: 115, - h: 23, - time: new Date('2016-05-09T14:57:51.238Z'), - msNormalized: 1744, - }; - - expect(events).toHaveLength(32); - expect(events[30]).toEqual(eventObj); - }); - - it('should fetch session content', async () => { - const provider = new EventProvider({ url: 'sample.com' }); - - jest - .spyOn(provider, '_fetchEvents') - .mockImplementation(() => - Promise.resolve(provider._createEvents(sample.events)) - ); - - jest - .spyOn(api, 'fetch') - .mockImplementation(() => Promise.resolve({ text: () => sample.data })); - - await provider.init(); - - expect(api.fetch).toHaveBeenCalledWith( - `sample.com/stream?offset=0&bytes=${MAX_SIZE}`, - { - Accept: 'text/plain', - 'Content-Type': 'text/plain; charset=utf-8', - } - ); - - const buf = new Buffer(sample.data); - const lastEvent = provider.events[provider.events.length - 2]; - const expectedChunk = buf - .slice(lastEvent.offset, lastEvent.offset + lastEvent.bytes) - .toString('utf8'); - expect(lastEvent.data).toEqual(expectedChunk); - }); -}); +import TtyPlayer, { StatusEnum } from './ttyPlayer'; describe('lib/ttyPlayer', () => { + let server; + const url = 'ws://localhost:3088'; + beforeEach(() => { + server = new WS(url); + }); afterEach(() => { + WS.clean(); + jest.clearAllMocks(); }); - it('should create an instance', () => { - const ttyPlayer = new TtyPlayer({ url: 'testSid' }); - expect(ttyPlayer.isReady()).toBe(false); - expect(ttyPlayer.isPlaying()).toBe(false); - expect(ttyPlayer.isError()).toBe(false); - expect(ttyPlayer.isLoading()).toBe(true); - expect(ttyPlayer.duration).toBe(0); - expect(ttyPlayer.current).toBe(0); - }); + it('connects to a websocket', async () => { + const setPlayerStatus = jest.fn(); + const setStatusText = () => {}; + const setTime = () => {}; - it('should connect using event provider', async () => { - const ttyPlayer = new TtyPlayer(new EventProvider({ url: 'testSid' })); - - jest.spyOn(api, 'get').mockImplementation(() => Promise.resolve(sample)); - jest - .spyOn(ttyPlayer._eventProvider, '_fetchContent') - .mockImplementation(() => Promise.resolve(sample.data)); - - await ttyPlayer.connect(); - - expect(ttyPlayer.isReady()).toBe(true); - expect(ttyPlayer.getEventCount()).toBe(32); - }); - - it('should indicate its loading status', async () => { - const ttyPlayer = new TtyPlayer(new EventProvider({ url: 'testSid' })); - jest - .spyOn(api, 'get') - .mockImplementation(() => Promise.resolve({ events: [] })); + const ttyPlayer = new TtyPlayer({ + url, + setPlayerStatus, + setStatusText, + setTime, + }); + const emit = jest.spyOn(ttyPlayer, 'emit'); ttyPlayer.connect(); - expect(ttyPlayer.isLoading()).toBe(true); + await server.connected; + + expect(setPlayerStatus.mock.calls).toHaveLength(1); + expect(setPlayerStatus.mock.calls[0][0]).toBe(StatusEnum.LOADING); + + expect(emit.mock.calls).toHaveLength(1); + expect(emit.mock.calls[0][0]).toBe('open'); + + server.close(); + await server.closed; + + expect(emit.mock.calls).toHaveLength(2); + expect(emit.mock.calls[1][0]).toBe(TermEvent.CONN_CLOSE); }); - it('should indicate its error status', async () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - jest.spyOn(api, 'get').mockImplementation(() => Promise.reject('!!')); + it('emits resize events', async () => { + const setPlayerStatus = () => {}; + const setStatusText = () => {}; + const setTime = () => {}; - const ttyPlayer = new TtyPlayer(new EventProvider({ url: 'testSid' })); + const ttyPlayer = new TtyPlayer({ + url, + setPlayerStatus, + setStatusText, + setTime, + }); + const emit = jest.spyOn(ttyPlayer, 'emit'); - await ttyPlayer.connect(); - expect(ttyPlayer.isError()).toBe(true); + ttyPlayer.connect(); + await server.connected; + + const resizeMessage = new Uint8Array([ + 5, // message type = Resize + 0, + 4, // size + 0, + 80, // width + 0, + 60, // height + ]); + + server.send(resizeMessage.buffer); + + expect(emit.mock.lastCall).toBeDefined(); + expect(emit.mock.lastCall[0]).toBe(TermEvent.RESIZE); + expect(emit.mock.lastCall[1]).toStrictEqual({ w: 80, h: 60 }); }); - describe('move()', () => { - var tty = null; + it('plays PTY data', async () => { + const setPlayerStatus = jest.fn(); + const setStatusText = jest.fn(); + const setTime = jest.fn(); - beforeEach(() => { - tty = new TtyPlayer(new EventProvider({ url: 'testSid' })); - jest.spyOn(api, 'get').mockImplementation(() => Promise.resolve(sample)); - jest - .spyOn(tty._eventProvider, '_fetchContent') - .mockImplementation(() => Promise.resolve(sample.data)); + const ttyPlayer = new TtyPlayer({ + url, + setPlayerStatus, + setStatusText, + setTime, }); + const emit = jest.spyOn(ttyPlayer, 'emit'); - afterEach(function () { - jest.clearAllMocks(); - }); + ttyPlayer.connect(); + await server.connected; - it('should move by 1 position when called w/o params', async () => { - await tty.connect(); + const data = new TextEncoder('utf-8').encode('~/test $'); + const len = data.length + 8; + const ptyMessage = new Uint8Array([ + 1 /* message type = PTY */, + len >> 8, + len & 0xff /* length */, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 123 /* timestamp (123ms) */, + ...data, + ]); - let renderedData = ''; - tty.on(TermEvent.DATA, data => { - renderedData = data; - }); + server.send(ptyMessage.buffer); - tty.move(); - expect(renderedData).toHaveLength(42); - }); + expect(emit.mock.lastCall).toBeDefined(); + expect(emit.mock.lastCall[0]).toBe(TermEvent.DATA); - it('should move from 1 to 478 position (forward)', async () => { - await tty.connect(); - - const renderedDataLength = []; - const resizeEvents = []; - - tty.on(TermEvent.RESIZE, event => { - resizeEvents.push(event); - }); - - tty.on(TermEvent.DATA, data => { - renderedDataLength.push(data.length); - tty.resumeFlow(); - }); - - tty.move(478); - - const expected = [ - { - resize: { - h: 20, - w: 147, - }, - length: 12899, - }, - { - resize: { - h: 29, - w: 146, - }, - length: 9415, - }, - { - resize: { - h: 31, - w: 146, - }, - length: 10113, - }, - { - resize: { - h: 25, - w: 146, - }, - length: 8018, - }, - ]; - - for (let i = 0; i < expected.length; i++) { - expect(resizeEvents[i]).toEqual(expected[i].resize); - expect(renderedDataLength[i]).toBe(expected[i].length); - } - }); - - it('should move from 478 to 1 position (back)', async () => { - await tty.connect(); - - let renderedData = ''; - tty.current = 478; - tty.on(TermEvent.DATA, data => { - renderedData = data; - }); - - tty.move(2); - expect(renderedData).toHaveLength(42); - }); - - it('should stop playing if new position is greater than session length', async () => { - await tty.connect(); - tty.play(); - - const someBigNumber = 20000; - tty.move(someBigNumber); - - expect(tty.isPlaying()).toBe(false); - }); + expect(emit.mock.lastCall[1]).toStrictEqual(Uint8Array.from(data).buffer); + expect(setTime.mock.lastCall).toBeDefined(); + expect(setTime.mock.lastCall[0]).toBe(123); }); }); diff --git a/web/packages/teleport/src/lib/term/ttyPlayerEventProvider.js b/web/packages/teleport/src/lib/term/ttyPlayerEventProvider.js deleted file mode 100644 index fdacbcee678..00000000000 --- a/web/packages/teleport/src/lib/term/ttyPlayerEventProvider.js +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import BufferModule from 'buffer/'; - -import api from 'teleport/services/api'; - -import { EventType } from './enums'; - -const URL_PREFIX_EVENTS = '/events'; -const Buffer = BufferModule.Buffer; - -export const MAX_SIZE = 5242880; // 5mg - -export default class EventProvider { - constructor({ url }) { - this.url = url; - this.events = []; - } - - getDuration() { - const eventCount = this.events.length; - if (eventCount === 0) { - return 0; - } - - return this.events[eventCount - 1].msNormalized; - } - - init() { - return this._fetchEvents().then(events => { - this.events = events; - const printEvents = this.events.filter(onlyPrintEvents); - if (printEvents.length === 0) { - return; - } - - return this._fetchContent(printEvents).then(buffer => { - this._populatePrintEvents(buffer, printEvents); - }); - }); - } - - _fetchEvents() { - const url = this.url + URL_PREFIX_EVENTS; - return api.get(url).then(json => { - if (!json.events) { - return []; - } - - return this._createEvents(json.events); - }); - } - - _fetchContent(events) { - // calculate the size of the session in bytes to know how many - // chunks to load due to maximum chunk size limitation. - let offset = events[0].offset; - const end = events.length - 1; - const totalSize = events[end].offset - offset + events[end].bytes; - const chunkCount = Math.ceil(totalSize / MAX_SIZE); - - // create a fetch request for each chunk - const promises = []; - for (let i = 0; i < chunkCount; i++) { - const url = `${this.url}/stream?offset=${offset}&bytes=${MAX_SIZE}`; - promises.push( - api - .fetch(url, { - Accept: 'text/plain', - 'Content-Type': 'text/plain; charset=utf-8', - }) - .then(response => response.text()) - ); - offset = offset + MAX_SIZE; - } - - // fetch all chunks and then merge - return Promise.all(promises).then(responses => { - const allBytes = responses.reduce((byteStr, r) => byteStr + r, ''); - return new Buffer(allBytes); - }); - } - - // assign a slice of tty stream to corresponding print event - _populatePrintEvents(buffer, events) { - let byteStrOffset = events[0].bytes; - events[0].data = buffer.slice(0, byteStrOffset).toString('utf8'); - for (var i = 1; i < events.length; i++) { - let { bytes } = events[i]; - events[i].data = buffer - .slice(byteStrOffset, byteStrOffset + bytes) - .toString('utf8'); - byteStrOffset += bytes; - } - } - - _createEvents(json) { - let w, h; - let events = []; - - // filter print events and ensure that each has the right screen size and valid values - for (let i = 0; i < json.length; i++) { - const { ms, event, offset, time, bytes } = json[i]; - - // grab new screen size for the next events - if (event === EventType.RESIZE || event === EventType.START) { - [w, h] = json[i].size.split(':'); - } - - // session has ended, stop here - if (event === EventType.END) { - const start = new Date(events[0].time); - const end = new Date(time); - const duration = end.getTime() - start.getTime(); - events.push({ - eventType: event, - ms: duration, - time: new Date(time), - }); - - break; - } - - // process only PRINT events - if (event !== EventType.PRINT) { - continue; - } - - events.push({ - eventType: EventType.PRINT, - ms, - bytes, - offset, - data: null, - w: Number(w), - h: Number(h), - time: new Date(time), - }); - } - - return this._normalizeEventsByTime(events); - } - - _normalizeEventsByTime(events) { - if (!events || events.length === 0) { - return []; - } - - events.forEach(e => { - e.displayTime = formatDisplayTime(e.ms); - e.ms = e.ms > 0 ? Math.floor(e.ms / 10) : 0; - e.msNormalized = e.ms; - }); - - let cur = events[0]; - let tmp = []; - for (let i = 1; i < events.length; i++) { - const sameSize = cur.w === events[i].w && cur.h === events[i].h; - const delay = events[i].ms - cur.ms; - - // merge events with tiny delay - if (delay < 2 && sameSize) { - cur.bytes += events[i].bytes; - continue; - } - - // avoid long delays between chunks - events[i].msNormalized = cur.msNormalized + shortenTime(delay); - - tmp.push(cur); - cur = events[i]; - } - - if (tmp.indexOf(cur) === -1) { - tmp.push(cur); - } - - return tmp; - } -} - -function shortenTime(value) { - if (value >= 25 && value < 50) { - return 25; - } else if (value >= 50 && value < 100) { - return 50; - } else if (value >= 100) { - return 100; - } else { - return value; - } -} - -function formatDisplayTime(ms) { - if (ms <= 0) { - return '00:00'; - } - - let totalSec = Math.floor(ms / 1000); - let totalDays = (totalSec % 31536000) % 86400; - let h = Math.floor(totalDays / 3600); - let m = Math.floor((totalDays % 3600) / 60); - let s = (totalDays % 3600) % 60; - - m = m > 9 ? m : '0' + m; - s = s > 9 ? s : '0' + s; - h = h > 0 ? h + ':' : ''; - - return `${h}${m}:${s}`; -} - -export function onlyPrintEvents(e) { - return e.eventType === EventType.PRINT; -} diff --git a/yarn.lock b/yarn.lock index 736d3cf8418..16fa035777a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10862,7 +10862,7 @@ jest-config@^29.7.0: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^29.7.0: +jest-diff@^29.2.0, jest-diff@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== @@ -11141,6 +11141,14 @@ jest-watcher@^29.7.0: jest-util "^29.7.0" string-length "^4.0.1" +jest-websocket-mock@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/jest-websocket-mock/-/jest-websocket-mock-2.5.0.tgz#9e0b07e270bed0224a6d3269fc62625eaa4d465c" + integrity sha512-a+UJGfowNIWvtIKIQBHoEWIUqRxxQHFx4CXT+R5KxxKBtEQ5rS3pPOV/5299sHzqbmeCzxxY5qE4+yfXePePig== + dependencies: + jest-diff "^29.2.0" + mock-socket "^9.3.0" + jest-worker@^26.5.0: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" @@ -12499,6 +12507,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mock-socket@^9.3.0: + version "9.3.1" + resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.3.1.tgz#24fb00c2f573c84812aa4a24181bb025de80cc8e" + integrity sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw== + module-details-from-path@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" @@ -16848,9 +16861,9 @@ webpack-virtual-modules@^0.4.1: integrity sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw== webpack@4, "webpack@>=4.43.0 <6.0.0", webpack@^5, webpack@^5.76.2, webpack@^5.88.2, webpack@^5.9.0: - version "5.88.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" - integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== + version "5.89.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" + integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.0"