Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ MATLAB® language server implements the Microsoft® [Language Server Proto

MATLAB language server requires MATLAB version R2021a or later.

**Note:** This extension will no longer support MATLAB R2021a in a future release. To make use of the advanced features of the extension or run MATLAB code, you will need to have MATLAB R2021b or later installed.

## Features Implemented
MATLAB language server implements several Language Server Protocol features and their related services:
* Code diagnostics — [publishDiagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics)
Expand All @@ -25,6 +27,20 @@ MATLAB language server supports these editors by installing the corresponding ex

### Unreleased

### 1.2.3
Release date: 2024-06-11

Notice:
* The MATLAB language server will no longer support MATLAB R2021a in a future release. To make use of the advanced features of the extension or run MATLAB code, you will need to have MATLAB R2021b or later installed.

Added:
* Added a system to detect if the connected MATLAB release is supported by the language server. This will inform the client, which may display a notification to the user about this.

Fixed:
* Resolved issue with connecting to Intel MATLAB installation on Apple Silicon machines
* Resolved error if MATLAB process is killed unexpectedly
* Fixed bug where "never" startup timing was ignored

### 1.2.2
Release date: 2024-05-17

Expand Down
53 changes: 35 additions & 18 deletions matlab/initmatlabls.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,72 @@ function initmatlabls (outFile)
% Prevent clearing the workspace from cleaning up the MatlabLanguageServerHelper
mlock

disp("matlabls: Beginning initialization")
fprintf("matlabls: matlabroot is \n%s\n", matlabroot)
disp('matlabls: Beginning initialization')
fprintf('matlabls: matlabroot is \n%s\n', matlabroot)

% Ensure the language server code is on the path
folder = fileparts(mfilename("fullpath"));
folder = fileparts(mfilename('fullpath'));
addpath(folder)

if isMATLABReleaseOlderThan('R2023a')
addpath(fullfile(folder, 'shadows', 'clc'));
try
if isMATLABReleaseOlderThan('R2023a')
addpath(fullfile(folder, 'shadows', 'clc'));
end
catch ME
disp('Error while attempting to add shadow directory to path')
disp(ME.message)
end

% Create matlabls helper for calculating language server operations
persistent matlablsHelper %#ok<PUSE>
matlablsHelper = matlabls.MatlabLanguageServerHelper();

try
matlablsHelper = matlabls.MatlabLanguageServerHelper();
catch ME
disp('Error instantiating MATLAB Language Server Helper:')
disp(ME.message)
end

if nargin == 1
logConnectionData(outFile)
end

disp("matlabls: Initialization complete")
disp('matlabls: Initialization complete')
end

function logConnectionData (outFile)
releaseInfo = matlabRelease;

data.pid = feature("getpid");
data.release = releaseInfo.Release;
data.pid = feature('getpid');
data.port = matlabls.internal.CommunicationManager.getSecurePort();
data.certFile = matlabls.internal.CommunicationManager.getCertificateLocation();
data.sessionKey = dduxinternal.getSessionKey();
try
releaseInfo = matlabRelease;
data.release = releaseInfo.Release;
catch
data.release = ['R' version('-release')];
end
try
data.sessionKey = dduxinternal.getSessionKey();
catch
data.sessionKey = 'Unknown - MATLAB too old';
end

connectionData = jsonencode(data);

disp(strcat("Printing connection data to file: ", newline, " ", outFile))
disp(strcat('Printing connection data to file: ', newline, ' ', outFile))

% Write data to a temporary file first, then move to the expected filename to
% avoid a timing issue where partial data may be read from the Node.js layer.
tmpFileName = strcat(outFile, '-tmp');

fid = fopen(tmpFileName, "w");
fid = fopen(tmpFileName, 'w');
if (fid == -1)
error("Failed to create temporary connection file.")
error('Failed to create temporary connection file.')
end

fprintf(fid, "%s\n", connectionData);
fprintf(fid, '%s\n', connectionData);
fclose(fid);

status = movefile(tmpFileName, outFile);
if ~status
error("Failed to rename connection file.")
error('Failed to rename connection file.')
end
end
32 changes: 16 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "matlab-language-server",
"version": "1.2.2",
"version": "1.2.3",
"description": "Language Server for MATLAB code",
"main": "./src/index.ts",
"bin": "./out/index.js",
Expand Down
2 changes: 1 addition & 1 deletion src/lifecycle/MatlabCommunicationManager.js

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions src/lifecycle/MatlabLifecycleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { EventEmitter } from 'events'

import ConfigurationManager, { Argument } from "./ConfigurationManager"
import ConfigurationManager, { Argument, ConnectionTiming } from "./ConfigurationManager"
import { MatlabConnection } from "./MatlabCommunicationManager"
import MatlabSession, { launchNewMatlab, connectToMatlab } from './MatlabSession'

Expand Down Expand Up @@ -39,7 +39,9 @@ class MatlabLifecycleManager {
}

// No connection currently established or establishing. Attempt to connect to MATLAB if desired.
if (startMatlab) {
const matlabConnectionTiming = (await ConfigurationManager.getConfiguration()).matlabConnectionTiming
const shouldStartMatlab = startMatlab && matlabConnectionTiming !== ConnectionTiming.Never
if (shouldStartMatlab) {
try {
const matlabSession = await this.connectToMatlab()
return matlabSession.getConnection()
Expand Down
39 changes: 27 additions & 12 deletions src/lifecycle/MatlabSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as fsPromises from 'fs/promises'
import * as os from 'os'
import * as path from 'path'
import { EventEmitter } from 'events'
import { checkIfMatlabDeprecated } from "../utils/DeprecationUtils";

interface MatlabStartupInfo {
pid: number
Expand Down Expand Up @@ -59,6 +60,10 @@ export async function launchNewMatlab (): Promise<MatlabSession> {
const connectionInfo = await readStartupInfo(outFile)
const { pid, release, port, certFile, sessionKey } = connectionInfo

// Check if the launched MATLAB is supported. We do not abort the connection, as this may
// be the user's desire and some functionality may work (althought it is not guaranteed).
checkIfMatlabDeprecated(release)

matlabSession.startConnection(port, certFile, pid, release).then(() => {
LifecycleNotificationHelper.notifyConnectionStatusChange(ConnectionState.CONNECTED)
Logger.log(`MATLAB session ${matlabSession.sessionId} connected to ${release}`)
Expand Down Expand Up @@ -100,8 +105,10 @@ export async function launchNewMatlab (): Promise<MatlabSession> {
matlabSession.initialize(matlabConnection, matlabProcess)

// Handles additional errors with launching the MATLAB process
matlabProcess?.on('error', error => {
matlabProcess.on('error', error => {
// Error occurred in child process
reject('Error from MATLAB child process')

matlabSession.shutdown('Error launching MATLAB')
watcher.close()

Expand All @@ -112,8 +119,16 @@ export async function launchNewMatlab (): Promise<MatlabSession> {

LifecycleNotificationHelper.didMatlabLaunchFail = true
NotificationService.sendNotification(Notification.MatlabLaunchFailed)
})

reject('Error from MATLAB child process')
// Handles the MATLAB process being terminated unexpectedly/externally.
// This could include the user killing the process.
matlabProcess.on('close', () => {
// Close connection
reject('MATLAB process terminated unexpectedly')

Logger.log(`MATLAB proces (session ${matlabSession.sessionId}) terminated`)
matlabSession.shutdown()
})
})
}
Expand Down Expand Up @@ -267,10 +282,18 @@ class LocalMatlabSession extends AbstractMatlabSession {
// Close the connection and kill MATLAB process
if (os.platform() === 'win32' && this.matlabPid != null) {
// Need to kill MATLAB's child process which is launched on Windows
process.kill(this.matlabPid, 'SIGTERM')
try {
process.kill(this.matlabPid, 'SIGTERM')
} catch {
Logger.warn('Unable to kill MATLAB child process - child process already killed')
}
}
this.matlabConnection?.close()
this.matlabProcess?.kill('SIGTERM')
try {
this.matlabProcess?.kill('SIGTERM')
} catch {
Logger.warn('Unable to kill MATLAB process - process already killed')
}
}

private setupListeners (): void {
Expand All @@ -281,14 +304,6 @@ class LocalMatlabSession extends AbstractMatlabSession {
const stderrStr: string = data.toString().trim()
Logger.writeMatlabLog(stderrStr)
})

// Handles the MATLAB process being terminated unexpectedly/externally.
// This could include the user killing the process.
this.matlabProcess?.on('close', () => {
// Close connection
Logger.log(`MATLAB process (session ${this.sessionId}) terminated`)
this.shutdown()
})

// Set up lifecycle listener
this.matlabConnection?.setLifecycleListener(lifecycleEvent => {
Expand Down
3 changes: 3 additions & 0 deletions src/notifications/NotificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export enum Notification {
MatlabFeatureUnavailable = 'feature/needsmatlab',
MatlabFeatureUnavailableNoMatlab = 'feature/needsmatlab/nomatlab',

// MATLAB Version Deprecation
MatlabVersionDeprecation = 'matlab/version/deprecation',

// Telemetry
LogTelemetryData = 'telemetry/logdata'
}
Expand Down
53 changes: 53 additions & 0 deletions src/utils/DeprecationUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2024 The MathWorks, Inc.

import Logger from "../logging/Logger"
import NotificationService, { Notification } from "../notifications/NotificationService"

const ORIGINAL_MIN_RELEASE = 'R2021a'
const CURRENT_MIN_RELEASE = 'R2021a'
const FUTURE_MIN_RELEASE = 'R2021b'

enum DeprecationType {
NEVER_SUPPORTED = 1,
DEPRECATED = 2,
TO_BE_DEPRECATED = 3
}

/**
* Checks if the given MATLAB release is unsupported, has been deprecated, or is planned
* for deprecation in a future release. If it falls under one of these categories, a
* notification is sent to the client, which may display a message to the user.
*
* @param matlabRelease The MATLAB release (e.g. "R2021a") which is being checked
*/
export function checkIfMatlabDeprecated (matlabRelease: string): void {
let deprecationType: DeprecationType

if (matlabRelease < ORIGINAL_MIN_RELEASE) {
// The launched MATLAB version has never been supported
deprecationType = DeprecationType.NEVER_SUPPORTED
Logger.error(`MATLAB ${matlabRelease} is not supported`)
} else if (matlabRelease >= ORIGINAL_MIN_RELEASE && matlabRelease < CURRENT_MIN_RELEASE) {
// The launched MATLAB version is no longer supported
deprecationType = DeprecationType.DEPRECATED
Logger.error(`MATLAB ${matlabRelease} is no longer supported`)
} else if (matlabRelease >= CURRENT_MIN_RELEASE && matlabRelease < FUTURE_MIN_RELEASE) {
// Support for the launched MATLAB version will end in an upcoming release
deprecationType = DeprecationType.TO_BE_DEPRECATED
Logger.warn(`Support for MATLAB ${matlabRelease} will end in a future update`)
} else {
// Support for the launched MATLAB version is not yet planned to end
return
}

let message = {
deprecationType: deprecationType,
deprecationInfo: {
matlabVersion: matlabRelease,
minVersion: CURRENT_MIN_RELEASE,
futureMinVersion: FUTURE_MIN_RELEASE
}
}

NotificationService.sendNotification(Notification.MatlabVersionDeprecation, message)
}