Skip to content

Commit

Permalink
Merge pull request #406 from refinery-labs/websocket-live-debugging
Browse files Browse the repository at this point in the history
Live Stream Lambda Output During Execution
  • Loading branch information
mandatoryprogrammer authored Oct 1, 2019
2 parents 369f8c9 + ac7ba2b commit b5f2f14
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 68 deletions.
9 changes: 9 additions & 0 deletions api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6305,6 +6305,15 @@ def post( self ):
lambda_info[ "arn" ],
execute_lambda_params
)

if "Task timed out after " in lambda_result[ "logs" ]:
logit( "Lambda timed out while being executed!" )
self.write({
"success": False,
"msg": "The Code Block timed out while running, you may have an infinite loop or you may need to increase your Code Block's Max Execution Time.",
"log_output": ""
})
raise gen.Return()

try:
return_data = json.loads(
Expand Down
40 changes: 0 additions & 40 deletions front-end/src/components/Common/Loading.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,43 +29,3 @@ export default class LoadingContainer extends Vue implements LoadingContainerPro
@Prop() classes?: string;
}
</script>

<style scoped lang="scss">
.loading-helper__container {
position: relative;
margin: 0;
max-width: unset;
}
.loading-helper__overlay {
background-color: #f0f0f0;
color: #000000;
z-index: 9998;
position: absolute;
top: 0;
left: 0;
margin: 0;
border: none;
width: 100%;
height: 100%;
opacity: 0.9;
&--dark {
background-color: #333;
color: #fff;
opacity: 0.7;
}
}
.loading-helper__loading-text {
text-align: center;
z-index: 9999;
top: 50%;
transform: translateY(-50%);
left: 0;
bottom: 0;
margin: auto;
position: relative;
font-weight: bold;
font-size: 16px;
}
</style>
10 changes: 8 additions & 2 deletions front-end/src/components/Common/RefineryCodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default class RefineryCodeEditor extends Vue implements EditorProps {
@Prop() onChange?: (s: string) => void;
@Prop() fullscreenToggled?: () => void;
@Prop({ default: false }) disableFullscreen!: boolean;
@Prop({ default: false }) tailOutput!: boolean;

// Ace Props
@Prop() readOnly?: boolean;
Expand Down Expand Up @@ -67,12 +68,17 @@ export default class RefineryCodeEditor extends Vue implements EditorProps {
wordWrap: this.wrapText,
theme: this.theme || 'vs-dark',
automaticLayout: true,
onChange: this.onChange
onChange: this.onChange,
tailOutput: this.tailOutput
};

return (
// @ts-ignore
<MonacoEditor key={`${languageToAceLangMap[this.lang]}${this.readOnly ? '-read-only' : ''}`} ref="editor" props={monacoProps} />
<MonacoEditor
key={`${languageToAceLangMap[this.lang]}${this.readOnly ? '-read-only' : ''}`}
ref="editor"
props={monacoProps}
/>
);
}

Expand Down
65 changes: 46 additions & 19 deletions front-end/src/components/RunLambda.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,35 @@ export default class RunLambda extends Vue implements RunLambdaProps {
}

// If we have valid output for a Lambda based on it's ID.
if (this.lambdaIdOrArn && this.runResultOutputId === this.lambdaIdOrArn) {
if (this.lambdaIdOrArn) {
return true;
}

// Otherwise, false. This is not valid output.
return false;
}

public getRunLambdaReturnFieldValue(runResultOutput: RunLambdaResult | null) {
// Check if the Lambda is running, we'll show a different return value if it is
// to indicate to the user that the Code Block is currently still executing.
if (this.isCurrentlyRunning) {
return 'The Code Block has not finished executing yet, please wait...';
}

if (runResultOutput && runResultOutput.returned_data && typeof runResultOutput.returned_data === 'string') {
return runResultOutput.returned_data;
}

return 'Click Execute button for run output.';
}

public getRunLambdaOutput(hasValidOutput: boolean) {
// Check if the Lambda is running, we'll show a different return value if it is
// to indicate to the user that the Code Block is currently still executing.
if (this.isCurrentlyRunning && !this.runResultOutput) {
return 'No output from Code Block received yet, please wait...';
}

// Need to check this because Ace will shit the bed if given a *gasp* null value!
if (!hasValidOutput || !this.runResultOutput) {
return 'No return data to display.';
Expand Down Expand Up @@ -126,6 +146,25 @@ export default class RunLambda extends Vue implements RunLambdaProps {
);
}

/*
Disables the Execute With Data button if the Lambda is running
*/
getExecuteWithDataButton() {
if (this.isCurrentlyRunning) {
return (
<b-button variant="primary" disabled={true}>
<b-spinner small /> Code Block is executing, please wait...
</b-button>
);
}

return (
<b-button variant="primary" on={{ click: () => this.onRunLambda() }}>
Execute With Data
</b-button>
);
}

public renderEditors() {
const hasValidOutput = this.checkIfValidRunLambdaOutput();

Expand All @@ -151,7 +190,7 @@ export default class RunLambda extends Vue implements RunLambdaProps {
name: `result-data-${this.getNameSuffix()}`,
// This is very nice for rendering non-programming text
lang: hasResultData ? 'json' : 'text',
content: hasResultData || 'Click Execute button for run output.',
content: this.getRunLambdaReturnFieldValue(this.runResultOutput),
wrapText: true,
readOnly: true
};
Expand All @@ -163,7 +202,8 @@ export default class RunLambda extends Vue implements RunLambdaProps {
lang: 'text',
content: this.getRunLambdaOutput(hasValidOutput),
wrapText: true,
readOnly: true
readOnly: true,
tailOutput: true
};

const saveInputDataButton = (
Expand All @@ -176,9 +216,7 @@ export default class RunLambda extends Vue implements RunLambdaProps {
<div class="m-2">
<b-button-group class="width--100percent">
{this.displayLocation === RunLambdaDisplayLocation.editor && this.onSaveInputData && saveInputDataButton}
<b-button variant="primary" on={{ click: () => this.onRunLambda() }}>
Execute With Data
</b-button>
{this.getExecuteWithDataButton()}
</b-button-group>
</div>
);
Expand Down Expand Up @@ -213,22 +251,11 @@ export default class RunLambda extends Vue implements RunLambdaProps {
}

public render(h: CreateElement): VNode {
const loadingProps: LoadingContainerProps = {
show: this.isCurrentlyRunning,
dark: true,
label: this.loadingText,
classes: 'height--100percent width--100percent'
};

const classes = {
'run-lambda-container display--flex flex-direction--column': true,
'run-lambda-container display--flex flex-direction--column width--100percent': true,
[`run-lambda-container__${this.displayMode}`]: true
};

return (
<Loading props={loadingProps}>
<div class={classes}>{this.renderEditors()}</div>
</Loading>
);
return <div class={classes}>{this.renderEditors()}</div>;
}
}
57 changes: 57 additions & 0 deletions front-end/src/lib/MonacoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Prop, Watch } from 'vue-property-decorator';
import elementResizeDetector from 'element-resize-detector';
import IModelContentChangedEvent = monaco.editor.IModelContentChangedEvent;
import { timeout } from '@/utils/async-utils';
import { IScrollEvent } from 'monaco-editor';

export interface MonacoEditorProps {
readOnly?: boolean;
Expand All @@ -17,13 +18,17 @@ export interface MonacoEditorProps {
diffEditor?: boolean;
wordWrap?: boolean;
automaticLayout?: boolean;
tailOutput?: boolean;

onChange?: (s: string) => void;
}

@Component
export default class MonacoEditor extends Vue implements MonacoEditorProps {
editor?: any;
lastEditorHeight?: number;
lastEditorContentsLength?: number;
tailingEnabled?: boolean;

@Prop() public readOnly?: boolean;
@Prop() public original?: string;
Expand All @@ -34,6 +39,7 @@ export default class MonacoEditor extends Vue implements MonacoEditorProps {
@Prop({ default: false }) public diffEditor!: boolean;
@Prop({ default: false }) public wordWrap!: boolean;
@Prop({ default: false }) public automaticLayout!: boolean;
@Prop({ default: false }) public tailOutput!: boolean;

@Prop() onChange?: (s: string) => void;

Expand All @@ -52,6 +58,8 @@ export default class MonacoEditor extends Vue implements MonacoEditorProps {
if (newValue !== editor.getValue()) {
editor.setValue(newValue);
}

this.onTailedContentRefreshed();
}
}

Expand All @@ -70,6 +78,34 @@ export default class MonacoEditor extends Vue implements MonacoEditorProps {
}
}

onTailedContentRefreshed() {
const editor = this.getModifiedEditor();
if (this.tailOutput) {
const characterCount = editor.getValue().length;
const editorPreviouslyHadContent = this.lastEditorContentsLength !== undefined;
const newContentIsShorter =
this.lastEditorContentsLength !== undefined && characterCount < this.lastEditorContentsLength;

// If we detected that we suddenly have less content in the editor
// than we did previously that means that we are almost certainly
// starting a new run and can re-enable tailing!
if (editorPreviouslyHadContent && newContentIsShorter) {
this.tailingEnabled = true;
}

this.lastEditorContentsLength = characterCount;
}

// We use tailingEnabled instead of the prop because
// we want to disable tailing if the user has attempted to scroll
// up. Vue doesn't like you mutating props so we pull the state internally.
if (this.tailingEnabled) {
// Auto-scroll to the bottom
const lineCount = editor.getModel().getLineCount();
editor.revealLine(lineCount);
}
}

relayoutEditor() {
this.editor.layout();
}
Expand Down Expand Up @@ -122,6 +158,27 @@ export default class MonacoEditor extends Vue implements MonacoEditorProps {
}
});

// Enable tailing if it was set
this.lastEditorHeight = 0;
if (this.tailOutput) {
this.tailingEnabled = true;
}

// This is used with the tailing of output functionality to calculate if
// a user scrolled while the output was being tailed. If they have and it
// wasn't programmatically-caused then we need to stop tailing!
editor.onDidScrollChange((event: IScrollEvent) => {
if (this.lastEditorHeight === undefined) {
return;
}

if (this.lastEditorHeight > event.scrollTop) {
this.tailingEnabled = false;
}

this.lastEditorHeight = event.scrollTop;
});

editor.updateOptions({
insertSpaces: true,
readOnly: this.readOnly
Expand Down
6 changes: 5 additions & 1 deletion front-end/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ import App from './App';
import router from './router';
import store from './store/index';
import './registerServiceWorker';
import VueNativeSock from 'vue-native-websocket';
import { setupWebsocketVuePlugin } from '@/setup-websocket';

Vue.use(BootstrapVue);
Vue.use(VueIntercom, { appId: 'sjaaunj7' });

Vue.config.productionTip = false;

setupWebsocketVuePlugin(Vue, store);

// If, in the future, we need to unsync the router we can use this function.
const unsync = sync(store, router);

Expand All @@ -48,7 +52,7 @@ window.onbeforeunload = function(e: Event) {
}
};

new Vue({
const vm = new Vue({
router,
store,
render: h => h(App)
Expand Down
25 changes: 25 additions & 0 deletions front-end/src/setup-websocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import VueNativeSock from 'vue-native-websocket';
import { RootState } from '@/store/store-types';
import { Vue, VueConstructor } from 'vue/types/vue';
import { Store } from 'vuex';

export function setupWebsocketVuePlugin(Vue: VueConstructor, store: Store<RootState>) {
const websocketEndpoint =
`${process.env.VUE_APP_API_HOST}`.replace('https://', 'wss://').replace('http://', 'ws://') +
'/ws/v1/lambdas/livedebug';

const mutations = {
SOCKET_ONOPEN: 'runLambda/SOCKET_ONOPEN',
SOCKET_ONCLOSE: 'runLambda/SOCKET_ONCLOSE',
SOCKET_ONERROR: 'runLambda/SOCKET_ONERROR',
SOCKET_ONMESSAGE: 'runLambda/SOCKET_ONMESSAGE',
SOCKET_RECONNECT: 'runLambda/SOCKET_RECONNECT',
SOCKET_RECONNECT_ERROR: 'runLambda/SOCKET_RECONNECT_ERROR'
};

Vue.use(VueNativeSock, websocketEndpoint, {
store: store,
reconnection: true,
mutations: mutations
});
}
Loading

0 comments on commit b5f2f14

Please sign in to comment.