Skip to content

Commit 0819c39

Browse files
diningPhilosopher64Prabhakar Kumar
authored andcommitted
Capability to communicate with MATLAB to terminate cleanly.
Status of MATLAB is based on ping instead of file existence.
1 parent ab58ffc commit 0819c39

31 files changed

+619
-124
lines changed

gui/src/actionCreators/actionCreators.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ describe.each([
2828
describe.each([
2929
[actionCreators.requestServerStatus, {type:actions.REQUEST_SERVER_STATUS}],
3030
[actionCreators.requestSetLicensing, {type:actions.REQUEST_SET_LICENSING}],
31-
[actionCreators.requestStopMatlab, {type:actions.REQUEST_STOP_MATLAB}],
32-
[actionCreators.requestStartMatlab, {type:actions.REQUEST_START_MATLAB}],
31+
[actionCreators.requestStopMatlab, {type:actions.REQUEST_STOP_MATLAB, status: 'stopping'}],
32+
[actionCreators.requestStartMatlab, {type:actions.REQUEST_START_MATLAB, status: 'starting'}],
3333
[actionCreators.requestTerminateIntegration, {type:actions.REQUEST_TERMINATE_INTEGRATION}],
3434
])('Test Request actionCreators', (method, expectedAction) => {
3535

gui/src/actionCreators/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export function receiveTerminateIntegration(status) {
101101
export function requestStopMatlab() {
102102
return {
103103
type: REQUEST_STOP_MATLAB,
104+
status: 'stopping'
104105
};
105106
}
106107

@@ -114,6 +115,7 @@ export function receiveStopMatlab(status) {
114115
export function requestStartMatlab() {
115116
return {
116117
type: REQUEST_START_MATLAB,
118+
status: 'starting'
117119
};
118120
}
119121

gui/src/components/Controls/Controls.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe('Controls Component', () => {
112112
fireEvent.click(confirmButton);
113113

114114
let tableData = container.querySelector('.details');
115-
expect(tableData.innerHTML).toMatch('Running');
115+
expect(tableData.innerHTML).toContain('Starting. This may take several minutes');
116116
});
117117

118118
});

gui/src/components/Controls/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
selectLicensingIsMhlm,
1010
selectLicensingProvided,
1111
selectMatlabRunning,
12+
selectMatlabStarting,
13+
selectMatlabStopping,
1214
selectMatlabVersion,
1315
selectError
1416
} from '../../selectors';
@@ -33,6 +35,8 @@ function Controls({
3335
const licensed = useSelector(selectLicensingProvided);
3436
const mhlmLicense = useSelector(selectLicensingIsMhlm);
3537
const matlabRunning = useSelector(selectMatlabRunning);
38+
const matlabStarting = useSelector(selectMatlabStarting);
39+
const matlabStopping = useSelector(selectMatlabStopping);
3640
const matlabVersion = useSelector(selectMatlabVersion);
3741
const error = useSelector(selectError);
3842

@@ -93,7 +97,7 @@ MATLAB version: ${matlabVersion}%0D%0A`,
9397
data-testid='startMatlabBtn'
9498
className={getBtnClass(matlabRunning ? 'restart' : 'start')}
9599
onClick={() => callback(Confirmations.START)}
96-
disabled={!licensed}
100+
disabled={!licensed || matlabStarting || matlabStopping}
97101
data-for="control-button-tooltip"
98102
data-tip={`${matlabRunning ? 'Restart' : 'Start'} your MATLAB session`}
99103
>

gui/src/reducers/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ export function matlabStatus(state = 'down', action) {
7878
case RECEIVE_STOP_MATLAB:
7979
case RECEIVE_START_MATLAB:
8080
return action.status.matlab.status;
81+
case REQUEST_STOP_MATLAB:
82+
case REQUEST_START_MATLAB:
83+
return action.status;
8184
default:
8285
return state;
8386
}

gui/src/selectors/index.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,17 @@ export const selectMatlabUp = createSelector(
4141

4242
export const selectMatlabRunning = createSelector(
4343
selectMatlabStatus,
44-
matlabStatus => matlabStatus === 'up' || matlabStatus === 'starting'
44+
matlabStatus => matlabStatus === 'up'
45+
);
46+
47+
export const selectMatlabStarting = createSelector(
48+
selectMatlabStatus,
49+
matlabStatus => matlabStatus === 'starting'
50+
);
51+
52+
export const selectMatlabStopping = createSelector(
53+
selectMatlabStatus,
54+
matlabStatus => matlabStatus === 'stopping'
4555
);
4656

4757
export const selectOverlayHidable = createSelector(
@@ -140,6 +150,14 @@ export const selectInformationDetails = createSelector(
140150
alert: 'info',
141151
spinner: true
142152
};
153+
154+
case 'stopping':
155+
return {
156+
label: 'Stopping',
157+
icon: 'info-reverse',
158+
alert: 'info',
159+
spinner: true
160+
};
143161
case 'down':
144162
const detail = {
145163
label: 'Not running',

gui/src/selectors/selectors.spec.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,20 +153,43 @@ describe('Test derived selectors', () => {
153153
});
154154

155155

156-
test('selectMatlabRunning should return true when Matlab is up or starting', () => {
156+
test('selectMatlabRunning should return true when Matlab is up', () => {
157157
expect(selectMatlabRunning(state)).toBe(true);
158158
modifiedState = _.cloneDeep(state);
159159
modifiedState.serverStatus.matlabStatus = 'starting';
160-
expect(selectMatlabRunning(modifiedState)).toBe(true);
160+
expect(selectMatlabRunning(modifiedState)).toBe(false);
161161
});
162162

163-
test('selectMatlabRunning should false when Matlab status is not up or starting', () => {
164-
163+
test('selectMatlabRunning should false when Matlab status is not up', () => {
165164
modifiedState = _.cloneDeep(state);
166165
modifiedState.serverStatus.matlabStatus = 'down';
167166
expect(selectMatlabRunning(modifiedState)).toBe(false);
168167
});
169168

169+
test('selectMatlabStopping should true when Matlab status is stopping', () => {
170+
modifiedState = _.cloneDeep(state);
171+
modifiedState.serverStatus.matlabStatus = 'stopping';
172+
expect(selectors.selectMatlabStopping(modifiedState)).toBe(true);
173+
});
174+
175+
test('selectMatlabStopping should false when Matlab status is not stopping', () => {
176+
modifiedState = _.cloneDeep(state);
177+
modifiedState.serverStatus.matlabStatus = 'up';
178+
expect(selectors.selectMatlabStopping(modifiedState)).toBe(false);
179+
});
180+
181+
test('selectMatlabStarting should true when Matlab status is starting', () => {
182+
modifiedState = _.cloneDeep(state);
183+
modifiedState.serverStatus.matlabStatus = 'starting';
184+
expect(selectors.selectMatlabStarting(modifiedState)).toBe(true);
185+
});
186+
187+
test('selectMatlabStarting should false when Matlab status is not starting', () => {
188+
modifiedState = _.cloneDeep(state);
189+
modifiedState.serverStatus.matlabStatus = 'up';
190+
expect(selectors.selectMatlabStopping(modifiedState)).toBe(false);
191+
});
192+
170193
test('selectOverlayHidable should return true when matlab is up and there is no error ', () => {
171194
expect(selectOverlayHidable(state)).toBe(true);
172195
});

matlab_proxy/app.py

Lines changed: 66 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
# nested subapp (with a name unlikely to cause collisions) containing this API and the
3232
# static serving.
3333

34+
# Setup logger for the integration and a web logger. Override any default loggers.
35+
logger = mwi.logger.get(init=True)
36+
3437

3538
def marshal_licensing_info(licensing_info):
3639
"""Gather/Marshal licensing information for MHLM and NLM Licensing types.
@@ -76,7 +79,7 @@ def marshal_error(error):
7679
}
7780

7881

79-
def create_status_response(app, loadUrl=None):
82+
async def create_status_response(app, loadUrl=None):
8083
"""Send a generic status response about the state of server,MATLAB and MATLAB Licensing
8184
8285
Args:
@@ -90,7 +93,7 @@ def create_status_response(app, loadUrl=None):
9093
return web.json_response(
9194
{
9295
"matlab": {
93-
"status": state.get_matlab_state(),
96+
"status": await state.get_matlab_state(),
9497
"version": state.settings["matlab_version"],
9598
},
9699
"licensing": marshal_licensing_info(state.licensing),
@@ -123,7 +126,7 @@ async def get_status(req):
123126
Returns:
124127
JSONResponse: JSONResponse object containing information about the server, MATLAB and MATLAB Licensing.
125128
"""
126-
return create_status_response(req.app)
129+
return await create_status_response(req.app)
127130

128131

129132
async def start_matlab(req):
@@ -140,7 +143,7 @@ async def start_matlab(req):
140143
# Start MATLAB
141144
await state.start_matlab(restart_matlab=True)
142145

143-
return create_status_response(req.app)
146+
return await create_status_response(req.app)
144147

145148

146149
async def stop_matlab(req):
@@ -156,7 +159,7 @@ async def stop_matlab(req):
156159

157160
await state.stop_matlab()
158161

159-
return create_status_response(req.app)
162+
return await create_status_response(req.app)
160163

161164

162165
async def set_licensing_info(req):
@@ -196,7 +199,7 @@ async def set_licensing_info(req):
196199
# Start MATLAB
197200
await state.start_matlab(restart_matlab=True)
198201

199-
return create_status_response(req.app)
202+
return await create_status_response(req.app)
200203

201204

202205
async def licensing_info_delete(req):
@@ -218,7 +221,7 @@ async def licensing_info_delete(req):
218221
# Persist licensing information
219222
state.persist_licensing()
220223

221-
return create_status_response(req.app)
224+
return await create_status_response(req.app)
222225

223226

224227
async def termination_integration_delete(req):
@@ -230,7 +233,7 @@ async def termination_integration_delete(req):
230233
state = req.app["state"]
231234

232235
# Send response manually because this has to happen before the application exits
233-
res = create_status_response(req.app, "../")
236+
res = await create_status_response(req.app, "../")
234237
await res.prepare(req)
235238
await res.write_eof()
236239

@@ -470,7 +473,7 @@ async def matlab_starter(app):
470473
state = app["state"]
471474

472475
try:
473-
if state.is_licensed() and state.get_matlab_state() == "down":
476+
if state.is_licensed() and await state.get_matlab_state() == "down":
474477
await state.start_matlab()
475478
except asyncio.CancelledError:
476479
# Ensure MATLAB is terminated
@@ -495,8 +498,12 @@ async def cleanup_background_tasks(app):
495498
Args:
496499
app (aiohttp_server): Instance of aiohttp server
497500
"""
498-
logger = mwi.logger.get()
501+
# First stop matlab
499502
state = app["state"]
503+
await state.stop_matlab()
504+
505+
# Stop any running async tasks
506+
logger = mwi.logger.get()
500507
tasks = state.tasks
501508
for task_name, task in tasks.items():
502509
if not task.cancelled():
@@ -507,10 +514,52 @@ async def cleanup_background_tasks(app):
507514
except asyncio.CancelledError:
508515
pass
509516

510-
await state.stop_matlab()
511517

518+
def configure_and_start(app):
519+
"""Configure the site for the app and update app with appropriate values
520+
521+
Args:
522+
app (aiohttp.web.Application): A aiohttp web server.
523+
524+
Returns:
525+
aiohttp.web.Application: Updated web server.
526+
"""
527+
loop = util.get_event_loop()
528+
529+
web_logger = None if not mwi_env.is_web_logging_enabled() else logger
530+
531+
# Setup runner
532+
runner = web.AppRunner(app, logger=web_logger, access_log=web_logger)
533+
loop.run_until_complete(runner.setup())
534+
535+
# Prepare site to start, then set port of the app.
536+
site = util.prepare_site(app, runner)
537+
538+
# This would be required when MWI_APP_PORT env variable is not set and the site starts on a random port.
539+
app["settings"]["app_port"] = site._port
540+
541+
# Update the site origin in settings.
542+
# The origin will be used for communicating with the Embedded connector.
543+
app["settings"]["mwi_server_url"] = site.name
544+
545+
loop.run_until_complete(site.start())
512546

513-
# config is has a default initializer because it needs to be callable without inputs from ADEV servers
547+
logger.debug("Starting MATLAB proxy app")
548+
logger.debug(
549+
f' with base_url: {app["settings"]["base_url"]} and app_port:{app["settings"]["app_port"]}.'
550+
)
551+
552+
logger.info(
553+
util.prettify(
554+
boundary_filler="=",
555+
text_arr=[f"MATLAB can be accessed at:", site.name],
556+
)
557+
)
558+
559+
return app
560+
561+
562+
# config has a default initializer because it needs to be callable without inputs from ADEV servers
514563
def create_app(config_name=matlab_proxy.get_default_config_name()):
515564
"""Creates the web server and adds the routes,settings and env_config to the server.
516565
@@ -558,51 +607,17 @@ def create_app(config_name=matlab_proxy.get_default_config_name()):
558607
def main():
559608
"""Starting point of the integration. Creates the web app and runs indefinitely."""
560609

561-
# Setup logger for the integration and a web logger. Override any default loggers.
562-
logger = mwi.logger.get(init=True)
563-
web_logger = None if not mwi_env.is_web_logging_enabled() else logger
564-
565610
# The integration needs to be called with --config flag.
566611
# Parse the passed cli arguments.
567612
desired_configuration_name = util.parse_cli_args()["config"]
568613

569-
app = create_app(desired_configuration_name)
570-
571-
loop = asyncio.get_event_loop()
572-
573-
# Setup runner
574-
runner = web.AppRunner(app, logger=web_logger, access_log=web_logger)
575-
loop.run_until_complete(runner.setup())
576-
577-
# Prepare site to start, then set port of the app.
578-
site = util.prepare_site(app, runner)
579-
# This would be required when MWI_APP_PORT env variable is not set and the site starts on a random port.
580-
app["settings"]["app_port"] = site._port
581-
loop.run_until_complete(site.start())
614+
# Create, configure and start the app.
615+
app = create_app(config_name=desired_configuration_name)
616+
app = configure_and_start(app)
582617

618+
loop = util.get_event_loop()
619+
# Add signal handlers for the current python process
583620
loop = util.add_signal_handlers(loop)
584-
585-
logger.debug("Starting MATLAB proxy app")
586-
logger.debug(
587-
f' with base_url: {app["settings"]["base_url"]} and app_port:{app["settings"]["app_port"]}.'
588-
)
589-
590-
ssl_context = app["settings"]["ssl_context"]
591-
if ssl_context != None:
592-
access_protocol = "https"
593-
else:
594-
access_protocol = "http"
595-
596-
logger.info(
597-
util.prettify(
598-
boundary_filler="=",
599-
text_arr=[
600-
f"MATLAB can be accessed at:",
601-
f'{access_protocol}://localhost:{app["settings"]["app_port"]}{app["settings"]["base_url"]}/index.html',
602-
],
603-
)
604-
)
605-
606621
loop.run_forever()
607622

608623
async def shutdown():

0 commit comments

Comments
 (0)