Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: mathworks/matlab-proxy
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.16.0
Choose a base ref
...
head repository: mathworks/matlab-proxy
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.17.0
Choose a head ref
  • 3 commits
  • 8 files changed
  • 3 contributors

Commits on May 10, 2024

  1. Workflows updated to use codecov/codecov-action@v4.

    Prabhakar Kumar committed May 10, 2024
    Copy the full SHA
    e98e7d0 View commit details

Commits on May 21, 2024

  1. Resets the active session when user closes the browser window.

    fixes: #32
    Shushant Singh authored and Prabhakar Kumar committed May 21, 2024
    Copy the full SHA
    cb20f0a View commit details
  2. Update to v0.17.0

    prabhakk-mw committed May 21, 2024
    Copy the full SHA
    fe1c044 View commit details
14 changes: 11 additions & 3 deletions .github/actions/generate-code-coverage/action.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# Copyright 2020-2023 The MathWorks, Inc.
# Copyright 2020-2024 The MathWorks, Inc.

# Composite Action to generate Code Coverage XML and Upload it
name: Generate Code Coverage XML

inputs:
codecov-token:
description: 'codecov.io token'
required: true

runs:
using: "composite"
steps:
@@ -27,13 +33,14 @@ runs:
shell: bash

- name: Upload python coverage report to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
directory: ./
name: Python-codecov
files: ./coverage.xml
fail_ci_if_error: true
verbose: true
token: ${{ inputs.codecov-token }}

- name: Install Node Dependencies
run: npm --prefix gui install gui
@@ -44,8 +51,9 @@ runs:
shell: bash

- name: Upload Javscript coverage report to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
directory: ./gui/coverage/
fail_ci_if_error: true
verbose: true
token: ${{ inputs.codecov-token }}
4 changes: 3 additions & 1 deletion .github/workflows/release-to-pypi.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2020-2024 The MathWorks, Inc
# Copyright 2020-2024 The MathWorks, Inc.

# Workflow to test MATLAB-Proxy while releasing to PyPi
name: Release to PyPI
@@ -23,6 +23,8 @@ jobs:

- name: Generate Code Coverage XML
uses: ./.github/actions/generate-code-coverage
with:
codecov-token: ${{ secrets.CODECOV_TOKEN }}


build_and_publish_pypi:
16 changes: 16 additions & 0 deletions gui/src/components/App/index.js
Original file line number Diff line number Diff line change
@@ -175,6 +175,22 @@ function App () {
}
}

useEffect(() => {
const handlePageHide = (event) => {
// Performs actions before the component unloads
if (isConcurrencyEnabled && !isSessionConcurrent && hasFetchedServerStatus) {
// A POST request to clear the active client when the tab/browser is closed
navigator.sendBeacon('./clear_client_id');
}
event.preventDefault();
event.returnValue = '';
};
window.addEventListener('pagehide', handlePageHide);
return () => {
window.removeEventListener('pagehide', handlePageHide);
};
}, [isConcurrencyEnabled, isSessionConcurrent, hasFetchedServerStatus]);

useEffect(() => {
// Initial fetch environment configuration
if (!hasFetchedEnvConfig) {
50 changes: 33 additions & 17 deletions matlab_proxy/app.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@
import pkgutil
import secrets
import sys
import uuid

import aiohttp
from aiohttp import client_exceptions, web
@@ -97,16 +96,15 @@ def marshal_error(error):


async def create_status_response(
app, loadUrl=None, client_id=None, transfer_session=False, is_desktop=False
app, loadUrl=None, client_id=None, is_active_client=None
):
"""Send a generic status response about the state of server, MATLAB, MATLAB Licensing and the client session status.
Args:
app (aiohttp.web.Application): Web Server
loadUrl (String, optional): Represents the root URL. Defaults to None.
client_id (String, optional): Represents the unique client_id when concurrency check is enabled. Defaults to None.
transfer_session (Boolean, optional): Represents whether the connection should be transfered or not when concurrency check is enabled. Defaults to False.
is_desktop (Boolean, optional): Represents whether the request is made by the desktop app or some other kernel. Defaults to False.
client_id (String, optional): Represents the generated client_id when concurrency check is enabled and client does not have a client_id. Defaults to None.
is_active_client (Boolean, optional): Represents whether the current client is the active_client when concurrency check is enabled. Defaults to None.
Returns:
JSONResponse: A JSONResponse object containing the generic state of the server, MATLAB, MATLAB Licensing and the client session status.
@@ -124,17 +122,30 @@ async def create_status_response(
"wsEnv": state.settings.get("ws_env", ""),
}

if IS_CONCURRENCY_CHECK_ENABLED and is_desktop:
if not client_id:
client_id = str(uuid.uuid4())
status["clientId"] = client_id
if client_id:
status["clientId"] = client_id
if is_active_client is not None:
status["isActiveClient"] = is_active_client

if not state.active_client or transfer_session:
state.active_client = client_id
return web.json_response(status)

status["isActiveClient"] = True if state.active_client == client_id else False

return web.json_response(status)
@token_auth.authenticate_access_decorator
async def clear_client_id(req):
"""API endpoint to reset the active client
Args:
req (HTTPRequest): HTTPRequest Object
Returns:
Response: an empty response in JSON format
"""
# Sleep for one second prior to clearing the client id to ensure that any remaining get_status responses are fully processed first.
await asyncio.sleep(1)
state = req.app["state"]
state.active_client = None
# This response is of no relevance to the front-end as the client has already exited
return web.json_response({})


@token_auth.authenticate_access_decorator
@@ -201,15 +212,17 @@ async def get_status(req):
JSONResponse: JSONResponse object containing information about the server, MATLAB and MATLAB Licensing.
"""
# The client sends the CLIENT_ID as a query parameter if the concurrency check has been set to true.
state = req.app["state"]
client_id = req.query.get("MWI_CLIENT_ID", None)
transfer_session = json.loads(req.query.get("TRANSFER_SESSION", "false"))
is_desktop = req.query.get("IS_DESKTOP", False)

generated_client_id, is_active_client = state.get_session_status(
is_desktop, client_id, transfer_session
)

return await create_status_response(
req.app,
client_id=client_id,
transfer_session=transfer_session,
is_desktop=is_desktop,
req.app, client_id=generated_client_id, is_active_client=is_active_client
)


@@ -773,6 +786,8 @@ async def cleanup_background_tasks(app):
# Stop any running async tasks
logger = mwi.logger.get()
tasks = state.tasks
if state.task_detect_client_status:
tasks["detect_client_status"] = state.task_detect_client_status
for task_name, task in tasks.items():
if not task.cancelled():
logger.debug(f"Cancelling MWI task: {task_name} : {task} ")
@@ -868,6 +883,7 @@ def create_app(config_name=matlab_proxy.get_default_config_name()):
app.router.add_route("GET", f"{base_url}/get_auth_token", get_auth_token)
app.router.add_route("GET", f"{base_url}/get_env_config", get_env_config)
app.router.add_route("PUT", f"{base_url}/start_matlab", start_matlab)
app.router.add_route("POST", f"{base_url}/clear_client_id", clear_client_id)
app.router.add_route("DELETE", f"{base_url}/stop_matlab", stop_matlab)
app.router.add_route("PUT", f"{base_url}/set_licensing_info", set_licensing_info)
app.router.add_route("PUT", f"{base_url}/update_entitlement", update_entitlement)
81 changes: 81 additions & 0 deletions matlab_proxy/app_state.py
Original file line number Diff line number Diff line change
@@ -9,11 +9,13 @@
from collections import deque
from datetime import datetime, timedelta, timezone
from typing import Final, Optional
import uuid

from matlab_proxy import util
from matlab_proxy.constants import (
CONNECTOR_SECUREPORT_FILENAME,
MATLAB_LOGS_FILE_NAME,
IS_CONCURRENCY_CHECK_ENABLED,
)
from matlab_proxy.settings import (
get_process_startup_timeout,
@@ -103,6 +105,12 @@ def __init__(self, settings):
# connected to the backend
self.active_client = None

# Used to detect whether the active client is actively sending out request or is inactive
self.active_client_request_detected = False

# An event loop task to handle the detection of client activity
self.task_detect_client_status = None

def __get_cached_config_file(self):
"""Get the cached config file
@@ -1304,3 +1312,76 @@ def parsed_errs():
if err is not None:
self.error = err
log_error(logger, err)

def get_session_status(self, is_desktop, client_id, transfer_session):
"""
Determines the session status for a client, potentially generating a new client ID.
This function is responsible for managing and tracking the session status of a client.
It can generate a new client ID if one is not provided and the conditions are met.
It also manages the active client status within the session, especially in scenarios
involving desktop clients and when concurrency checks are enabled.
Args:
is_desktop (bool): A flag indicating whether the client is a desktop client.
client_id (str or None): The client ID. If None, a new client ID may be generated.
transfer_session (bool): Indicates whether the session should be transferred to this client.
Returns:
tuple:
- A 2-tuple containing the generated client ID (or None if not generated) and
a boolean indicating whether the client is considered the active client.
- If concurrency checks are not enabled or the client is not a desktop client, it returns None for both
the generated client ID and the active client status.
"""
if IS_CONCURRENCY_CHECK_ENABLED and is_desktop:
generated_client_id = None
if not client_id:
generated_client_id = str(uuid.uuid4())
client_id = generated_client_id

if not self.active_client or transfer_session:
self.active_client = client_id

if not self.task_detect_client_status:
# Create the loop to detect the active status of the client
loop = util.get_event_loop()
self.task_detect_client_status = loop.create_task(
self.detect_active_client_status()
)

if self.active_client == client_id:
is_active_client = True
self.active_client_request_detected = True
else:
is_active_client = False
return generated_client_id, is_active_client
return None, None

async def detect_active_client_status(self, sleep_time=1, max_inactive_count=10):
"""Detects whether the client is online or not by continuously checking if the active client is making requests
Args:
sleep_time (int): The time in seconds for which the process waits before checking for the next get_status request from the active client.
max_inactive_count (int): The maximum number of times the check for the request from the active_client fails before reseting the active client id.
"""
inactive_count = 0
while self.active_client:
# Check if the get_status request from the active client is received or not
await asyncio.sleep(sleep_time)
if self.active_client_request_detected:
self.active_client_request_detected = False
inactive_count = 0
else:
inactive_count = inactive_count + 1
if inactive_count > max_inactive_count:
# If no request is received from the active_client for more than 10 seconds then clear the active client id
inactive_count = 0
self.active_client = None
if self.task_detect_client_status:
try:
# Self cleanup of the task
self.task_detect_client_status.cancel()
self.task_detect_client_status = None
except Exception as e:
logger.error("Cleaning of task: 'detect_client_status' failed.")
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -70,7 +70,7 @@ def run(self):

setuptools.setup(
name="matlab-proxy",
version="0.16.0",
version="0.17.0",
url=config["doc_url"],
author="The MathWorks, Inc.",
author_email="cloud@mathworks.com",
14 changes: 14 additions & 0 deletions tests/unit/test_app.py
Original file line number Diff line number Diff line change
@@ -284,6 +284,20 @@ async def test_get_status_route(test_server):
assert resp.status == HTTPStatus.OK


async def test_clear_client_id_route(test_server):
"""Test to check endpoint: "/clear_client_id"
Args:
test_server (aiohttp_client): A aiohttp_client server for sending POST request.
"""

state = test_server.server.app["state"]
state.active_client = "mock_client_id"
resp = await test_server.post("/clear_client_id")
assert resp.status == HTTPStatus.OK
assert state.active_client is None


async def test_get_env_config(test_server):
"""Test to check endpoint : "/get_env_config"
Loading