diff --git a/extensions/eclipse-che-theia-activity-tracker/.gitignore b/extensions/eclipse-che-theia-activity-tracker/.gitignore new file mode 100644 index 000000000..c3af85790 --- /dev/null +++ b/extensions/eclipse-che-theia-activity-tracker/.gitignore @@ -0,0 +1 @@ +lib/ diff --git a/extensions/eclipse-che-theia-activity-tracker/README.md b/extensions/eclipse-che-theia-activity-tracker/README.md new file mode 100644 index 000000000..261c79005 --- /dev/null +++ b/extensions/eclipse-che-theia-activity-tracker/README.md @@ -0,0 +1,34 @@ +# che-theia-activity-tracker extension + +This extension tracks user activity and sends notification about that to Che workspace master to prevent stop of workspace due to inactivity timeout. + +### What types of activity are tracked? + +This extension tracks a key press, mouse clicks and mouse move. + +### How does the extension use the tracked data? + +The extension doesn't save or collect any data at all, it even doesn't distinguish between activity types. Only the fact of any activity matters. + +## Technical info + +### How heavy it is? + +Activity events handlers are triggered quite often, but they are very lightweight, so the extension does not affect IDE performance much. + +### How often are requests sent? + +The requests strategy is well optimized on both frontend and backend side. + +Frontend side in case of regular activity postpones sending request to backend for some period of time (1 minute by default). +So, when a user just works in the IDE only one request will be sent during the period. +But if any activity happens after idling then request will be sent immediately. +However, this approach has one small drawback. It might make inactivity timeout up to the period longer (the worst case when user makes an activity event right after request to backend, so that activity is tracked and will be sent only after current period). + +Backend works similar to frontend, but instead of user activity events it gets requests from frontend. This is useful when backend has a few frontends, so in that case only one request will be sent to workspace master. Also, unlike frontend, it has additional mechanism for resending requests if some network error occurs. + +### How to test the extension + +The easiest way is to set workspace inactive timeout for 2 minutes and spend 4 minutes in the IDE. If the workspace is still running, it means that the extension works. + +To edit workspace inactive timeout set `CHE_LIMITS_WORKSPACE_IDLE_TIMEOUT` (value is in milliseconds) in `che.env` and restart Che. diff --git a/extensions/eclipse-che-theia-activity-tracker/package.json b/extensions/eclipse-che-theia-activity-tracker/package.json new file mode 100644 index 000000000..330953dd6 --- /dev/null +++ b/extensions/eclipse-che-theia-activity-tracker/package.json @@ -0,0 +1,29 @@ +{ + "name": "@eclipse-che/theia-activity-tracker", + "keywords": [ + "theia-extension" + ], + "version": "1.0.0", + "files": [ + "lib", + "src" + ], + "dependencies": { + "@theia/core": "next" + }, + "scripts": { + "prepare": "yarn clean && yarn build", + "clean": "rimraf lib", + "format": "tsfmt -r --useTsfmt ../../configs/tsfmt.json", + "lint": "tslint -c ../../configs/tslint.json --project tsconfig.json", + "compile": "tsc", + "build": "concurrently -n \"format,lint,compile\" -c \"red,green,blue\" \"yarn format\" \"yarn lint\" \"yarn compile\"", + "watch": "tsc -w" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/che-theia-activity-tracker-frontend-module", + "backend": "lib/node/che-theia-activity-tracker-server-module" + } + ] +} diff --git a/extensions/eclipse-che-theia-activity-tracker/src/browser/che-theia-activity-tracker-contribution.ts b/extensions/eclipse-che-theia-activity-tracker/src/browser/che-theia-activity-tracker-contribution.ts new file mode 100644 index 000000000..181591516 --- /dev/null +++ b/extensions/eclipse-che-theia-activity-tracker/src/browser/che-theia-activity-tracker-contribution.ts @@ -0,0 +1,75 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { ActivityTrackerService } from '../common/activity-tracker-protocol'; + +/** + * Client side part of Theia activity tracker. + * Treats any key press or mouse event as an activity and sends activity update to Theia backed. + * To avoid flood of backend, sends updates periodially, unless last activity was detected after idling. + * Might keep workspace alive on the period longer. + */ +@injectable() +export class CheTheiaActivityTrackerFrontendContribution implements FrontendApplicationContribution { + + // Period for which all activity events turn into one request to backend. + private static REQUEST_PERIOD_MS = 1 * 60 * 1000; + + private isAnyActivity: boolean; + private isTimerRunning: boolean; + + constructor( + @inject(ActivityTrackerService) private activityService: ActivityTrackerService + ) { + this.isAnyActivity = false; + this.isTimerRunning = false; + } + + initialize(): void { + window.addEventListener('keydown', () => this.onAnyActivity()); + window.addEventListener('mousedown', () => this.onAnyActivity()); + window.addEventListener('mousemove', () => this.onAnyActivity()); + + // is needed if user reopens browser tab + this.sendRequestAndSetTimer(); + } + + private onAnyActivity(): void { + this.isAnyActivity = true; + + if (!this.isTimerRunning) { + this.sendRequestAndSetTimer(); + } + } + + private sendRequestAndSetTimer(): void { + this.activityService.resetTimeout(); + this.isAnyActivity = false; + + setTimeout(() => this.checkActivityTimerCallback(), CheTheiaActivityTrackerFrontendContribution.REQUEST_PERIOD_MS); + this.isTimerRunning = true; + } + + private checkActivityTimerCallback(): void { + this.isTimerRunning = false; + if (this.isAnyActivity) { + this.sendRequestAndSetTimer(); + } + } + +} diff --git a/extensions/eclipse-che-theia-activity-tracker/src/browser/che-theia-activity-tracker-frontend-module.ts b/extensions/eclipse-che-theia-activity-tracker/src/browser/che-theia-activity-tracker-frontend-module.ts new file mode 100644 index 000000000..53d98fd03 --- /dev/null +++ b/extensions/eclipse-che-theia-activity-tracker/src/browser/che-theia-activity-tracker-frontend-module.ts @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { WebSocketConnectionProvider, FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { ActivityTrackerService, ACTIVITY_TRACKER_SERVICE_PATH } from '../common/activity-tracker-protocol'; +import { CheTheiaActivityTrackerFrontendContribution } from './che-theia-activity-tracker-contribution'; + +export default new ContainerModule(bind => { + bind(ActivityTrackerService).toDynamicValue(context => + context.container.get(WebSocketConnectionProvider).createProxy(ACTIVITY_TRACKER_SERVICE_PATH) + ).inSingletonScope(); + + bind(FrontendApplicationContribution).to(CheTheiaActivityTrackerFrontendContribution); +}); diff --git a/extensions/eclipse-che-theia-activity-tracker/src/common/activity-tracker-protocol.ts b/extensions/eclipse-che-theia-activity-tracker/src/common/activity-tracker-protocol.ts new file mode 100644 index 000000000..3102ff0ea --- /dev/null +++ b/extensions/eclipse-che-theia-activity-tracker/src/common/activity-tracker-protocol.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +export const ACTIVITY_TRACKER_SERVICE_PATH = '/services/activity-tracker'; + +export const ActivityTrackerService = Symbol('ActivityTrackerService'); + +export interface ActivityTrackerService { + /** + * Asks backend to reset current workspace inactivity timeout. + */ + resetTimeout(): void; +} diff --git a/extensions/eclipse-che-theia-activity-tracker/src/node/activity-tracker-service.ts b/extensions/eclipse-che-theia-activity-tracker/src/node/activity-tracker-service.ts new file mode 100644 index 000000000..82aa31a42 --- /dev/null +++ b/extensions/eclipse-che-theia-activity-tracker/src/node/activity-tracker-service.ts @@ -0,0 +1,129 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { ActivityTrackerService } from '../common/activity-tracker-protocol'; +import * as http from 'http'; + +/** + * Server side part of Theia activity tracker. + * Receives activity updates from clients and sends reset inactivity requests to Che workspace master. + * To avoid duplicate requests from different frontend clients may send requests periodically. This mean + * that, in the worst case, it might keep user's workspace alive for the period longer. + * Che master API url for resetting inactive timeout: che-host[:port]/api/activity/ + */ +@injectable() +export class ActivityTrackerServiceImpl implements ActivityTrackerService { + + private static WORKSPACE_ID_ENV_VAR_NAME = 'CHE_WORKSPACE_ID'; + private static MACHINE_TOKEN_ENV_VAR_NAME = 'CHE_MACHINE_TOKEN'; + private static CHE_API_ENV_VAR_NAME = 'CHE_API_INTERNAL'; + + // Time before sending next request. If a few requests from frontend(s) are recieved during this period, + // only one request to workspace master will be sent. + private static REQUEST_PERIOD_MS = 1 * 60 * 1000; + // Time before resending request to workspace master if a network error occurs. + private static RETRY_REQUEST_PERIOD_MS = 5 * 1000; + // Number of retries before give up if a network error occurs. + private static RETRY_COUNT = 5; + + // Indicates state of the timer. If true timer is running. + private isTimerRunning: boolean; + // Flag which is used to check if new requests were recieved during timer awaiting. + private isNewRequest: boolean; + + // http or https module to make requestst to Che API. + private pinger: { request(option: http.RequestOptions): http.ClientRequest }; + private activityRequestOptions: http.RequestOptions; + + constructor() { + const workspaceId = process.env[ActivityTrackerServiceImpl.WORKSPACE_ID_ENV_VAR_NAME]; + if (!workspaceId) { + throw new Error('Cannot retrieve workspace ID'); + } + + const apiUrlString = process.env[ActivityTrackerServiceImpl.CHE_API_ENV_VAR_NAME]; + if (!apiUrlString) { + throw new Error('Cannot retrieve Che API uri'); + } + + const apiUrl = new URI(apiUrlString); + this.activityRequestOptions = { + path: apiUrl.path + '/activity/' + workspaceId, + method: 'PUT' + }; + const authorityColonPos = apiUrl.authority.indexOf(':'); + if (authorityColonPos === -1) { + this.activityRequestOptions.hostname = apiUrl.authority; + this.activityRequestOptions.port = apiUrl.scheme === 'http' ? 80 : 443; + } else { + this.activityRequestOptions.hostname = apiUrl.authority.substring(0, authorityColonPos); + this.activityRequestOptions.port = apiUrl.authority.substring(authorityColonPos + 1); + } + + const token = process.env[ActivityTrackerServiceImpl.MACHINE_TOKEN_ENV_VAR_NAME]; + if (token) { + this.activityRequestOptions.headers = { 'Authorization': 'Bearer ' + token }; + } + + this.pinger = apiUrl.scheme === 'http' ? require('http') : require('https'); + + this.isTimerRunning = false; + this.isNewRequest = false; + } + + /** + * Invoked each time when a client sends an activity request. + */ + resetTimeout(): void { + if (this.isTimerRunning) { + this.isNewRequest = true; + return; + } + + this.sendRequestAndSetTimer(); + } + + private sendRequestAndSetTimer(): void { + this.sendRequest(); + this.isNewRequest = false; + + setTimeout(() => this.checkNewRequestsTimerCallback(), ActivityTrackerServiceImpl.REQUEST_PERIOD_MS); + this.isTimerRunning = true; + } + + private checkNewRequestsTimerCallback(): void { + this.isTimerRunning = false; + + if (this.isNewRequest) { + this.sendRequestAndSetTimer(); + } + } + + private sendRequest(attemptsLeft: number = ActivityTrackerServiceImpl.RETRY_COUNT): void { + const request = this.pinger.request(this.activityRequestOptions); + request.on('error', (error: Error) => { + if (attemptsLeft > 0) { + setTimeout(() => this.sendRequest(), ActivityTrackerServiceImpl.RETRY_REQUEST_PERIOD_MS, --attemptsLeft); + } else { + console.error('Activity tracker: Failed to ping workspace master: ', error.message); + } + }); + request.end(); + } + +} diff --git a/extensions/eclipse-che-theia-activity-tracker/src/node/che-theia-activity-tracker-server-module.ts b/extensions/eclipse-che-theia-activity-tracker/src/node/che-theia-activity-tracker-server-module.ts new file mode 100644 index 000000000..8ebe975bf --- /dev/null +++ b/extensions/eclipse-che-theia-activity-tracker/src/node/che-theia-activity-tracker-server-module.ts @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core/lib/common'; +import { ActivityTrackerService, ACTIVITY_TRACKER_SERVICE_PATH } from '../common/activity-tracker-protocol'; +import { ActivityTrackerServiceImpl } from './activity-tracker-service'; + +export default new ContainerModule(bind => { + bind(ActivityTrackerService).to(ActivityTrackerServiceImpl).inSingletonScope(); + + bind(ConnectionHandler).toDynamicValue(context => + new JsonRpcConnectionHandler(ACTIVITY_TRACKER_SERVICE_PATH, () => context.container.get(ActivityTrackerService)) + ).inSingletonScope(); +}); diff --git a/extensions/eclipse-che-theia-activity-tracker/tsconfig.json b/extensions/eclipse-che-theia-activity-tracker/tsconfig.json new file mode 100644 index 000000000..a6a832536 --- /dev/null +++ b/extensions/eclipse-che-theia-activity-tracker/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../configs/base.tsconfig.json", + "compilerOptions": { + "lib": [ + "es6", + "dom" + ], + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ] +} diff --git a/extensions/extensions.yml b/extensions/extensions.yml index 453268694..292d7adf5 100644 --- a/extensions/extensions.yml +++ b/extensions/extensions.yml @@ -1,12 +1,3 @@ -extensions: -- source: https://github.com/eclipse/che-theia-task-plugin - folders: - - che-theia-task-extension - checkoutTo: master -- source: https://github.com/eclipse/che-theia-activity-tracker - folders: - - che-theia-activity-tracker - checkoutTo: master - source: https://github.com/eclipse/che-theia folders: - dockerfiles/theia-endpoint-runtime @@ -16,4 +7,5 @@ extensions: - extensions/eclipse-che-theia-user-preferences - extensions/che-theia-hosted-plugin-manager-extension - extensions/eclipse-che-theia-dashboard + - extensions/eclipse-che-theia-activity-tracker checkoutTo: master diff --git a/yarn.lock b/yarn.lock index d129e3ed7..a5a7fae26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6816,9 +6816,9 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" -"moxios@git://github.com/stoplightio/moxios.git#v1.3.0": +"moxios@git://github.com/stoplightio/moxios#v1.3.0": version "1.3.0" - resolved "git://github.com/stoplightio/moxios.git#9d702c8eafee4b02917d6bc400ae15f1e835cf51" + resolved "git://github.com/stoplightio/moxios#9d702c8eafee4b02917d6bc400ae15f1e835cf51" dependencies: class-autobind "^0.1.4"