Skip to content

Commit d0da32c

Browse files
authored
Refactor artifact handling into shared ArtifactHandler class (compiler-explorer#7825)
1 parent d10e5fb commit d0da32c

File tree

3 files changed

+354
-330
lines changed

3 files changed

+354
-330
lines changed

static/artifact-handler.ts

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
// Copyright (c) 2025, Compiler Explorer Authors
2+
// All rights reserved.
3+
//
4+
// Redistribution and use in source and binary forms, with or without
5+
// modification, are permitted provided that the following conditions are met:
6+
//
7+
// * Redistributions of source code must retain the above copyright notice,
8+
// this list of conditions and the following disclaimer.
9+
// * Redistributions in binary form must reproduce the above copyright
10+
// notice, this list of conditions and the following disclaimer in the
11+
// documentation and/or other materials provided with the distribution.
12+
//
13+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14+
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16+
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
17+
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18+
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19+
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21+
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22+
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
23+
// POSSIBILITY OF SUCH DAMAGE.
24+
25+
import {Buffer} from 'buffer';
26+
import $ from 'jquery';
27+
import {CompilationResult} from '../types/compilation/compilation.interfaces.js';
28+
import {Artifact, ArtifactType} from '../types/tool.interfaces.js';
29+
import {assert, unwrap} from './assert.js';
30+
import * as BootstrapUtils from './bootstrap-utils.js';
31+
import {Alert} from './widgets/alert.js';
32+
33+
export class ArtifactHandler {
34+
private alertSystem: Alert;
35+
36+
constructor(alertSystem: Alert) {
37+
this.alertSystem = alertSystem;
38+
}
39+
40+
handle(result: CompilationResult): void {
41+
if (result.artifacts) {
42+
for (const artifact of result.artifacts) {
43+
this.handleArtifact(artifact);
44+
}
45+
} else if (result.execResult) {
46+
// Handle executor results recursively
47+
this.handle(result.execResult);
48+
}
49+
}
50+
51+
private handleArtifact(artifact: Artifact): void {
52+
switch (artifact.type) {
53+
case ArtifactType.nesrom:
54+
this.emulateNESROM(artifact);
55+
break;
56+
case ArtifactType.bbcdiskimage:
57+
this.emulateBbcDisk(artifact);
58+
break;
59+
case ArtifactType.zxtape:
60+
this.emulateSpeccyTape(artifact);
61+
break;
62+
case ArtifactType.smsrom:
63+
this.emulateMiracleSMS(artifact);
64+
break;
65+
case ArtifactType.timetrace:
66+
this.offerViewInSpeedscope(artifact);
67+
break;
68+
case ArtifactType.c64prg:
69+
this.emulateC64Prg(artifact);
70+
break;
71+
case ArtifactType.heaptracktxt:
72+
this.offerViewInSpeedscope(artifact);
73+
break;
74+
case ArtifactType.gbrom:
75+
this.emulateGameBoyROM(artifact);
76+
break;
77+
}
78+
}
79+
80+
private offerViewInSpeedscope(artifact: Artifact): void {
81+
this.alertSystem.notify(
82+
'Click ' +
83+
'<a class="link-primary" target="_blank" id="download_link" style="cursor:pointer;">here</a>' +
84+
' to view ' +
85+
artifact.title +
86+
' in Speedscope',
87+
{
88+
group: artifact.type,
89+
collapseSimilar: false,
90+
dismissTime: 10000,
91+
onBeforeShow: elem => {
92+
elem.find('#download_link').on('click', () => {
93+
const tmstr = Date.now();
94+
const live_url = 'https://static.ce-cdn.net/speedscope/index.html';
95+
const speedscope_url =
96+
live_url +
97+
'?' +
98+
tmstr +
99+
'#customFilename=' +
100+
encodeURIComponent(artifact.name) +
101+
'&b64data=' +
102+
encodeURIComponent(artifact.content);
103+
window.open(speedscope_url);
104+
});
105+
},
106+
},
107+
);
108+
}
109+
110+
offerViewInPerfetto(artifact: Artifact): void {
111+
this.alertSystem.notify(
112+
'Click ' +
113+
'<a class="link-primary" target="_blank" id="download_link" style="cursor:pointer;">here</a>' +
114+
' to view ' +
115+
artifact.title +
116+
' in Perfetto',
117+
{
118+
group: artifact.type,
119+
collapseSimilar: false,
120+
dismissTime: 10000,
121+
onBeforeShow: elem => {
122+
elem.find('#download_link').on('click', () => {
123+
const perfetto_url = 'https://ui.perfetto.dev';
124+
const win = window.open(perfetto_url);
125+
if (win) {
126+
const timer = setInterval(() => win.postMessage('PING', perfetto_url), 50);
127+
128+
const onMessageHandler = (evt: MessageEvent) => {
129+
if (evt.data !== 'PONG') return;
130+
clearInterval(timer);
131+
132+
const data = {
133+
perfetto: {
134+
buffer: Buffer.from(artifact.content, 'base64'),
135+
title: artifact.name,
136+
filename: artifact.name,
137+
},
138+
};
139+
win.postMessage(data, perfetto_url);
140+
};
141+
window.addEventListener('message', onMessageHandler);
142+
}
143+
});
144+
},
145+
},
146+
);
147+
}
148+
149+
private emulateMiracleSMS(artifact: Artifact): void {
150+
const dialog = $('#miracleemu');
151+
152+
this.alertSystem.notify(
153+
'Click ' +
154+
'<a target="_blank" id="miracle_emulink" style="cursor:pointer;" class="link-primary">here</a>' +
155+
' to emulate',
156+
{
157+
group: 'emulation',
158+
collapseSimilar: true,
159+
dismissTime: 10000,
160+
onBeforeShow: elem => {
161+
elem.find('#miracle_emulink').on('click', () => {
162+
BootstrapUtils.showModal(dialog);
163+
164+
const miracleMenuFrame = dialog.find('#miracleemuframe')[0];
165+
assert(miracleMenuFrame instanceof HTMLIFrameElement);
166+
if ('contentWindow' in miracleMenuFrame) {
167+
const emuwindow = unwrap(miracleMenuFrame.contentWindow);
168+
const tmstr = Date.now();
169+
emuwindow.location =
170+
'https://xania.org/miracle/miracle.html?' +
171+
tmstr +
172+
'#b64sms=' +
173+
encodeURIComponent(artifact.content);
174+
}
175+
});
176+
},
177+
},
178+
);
179+
}
180+
181+
private emulateSpeccyTape(artifact: Artifact): void {
182+
const dialog = $('#jsspeccyemu');
183+
184+
this.alertSystem.notify(
185+
'Click ' +
186+
'<a target="_blank" id="jsspeccy_emulink" style="cursor:pointer;" class="link-primary">here</a>' +
187+
' to emulate',
188+
{
189+
group: 'emulation',
190+
collapseSimilar: true,
191+
dismissTime: 10000,
192+
onBeforeShow: elem => {
193+
elem.find('#jsspeccy_emulink').on('click', () => {
194+
BootstrapUtils.showModal(dialog);
195+
196+
const speccyemuframe = dialog.find('#speccyemuframe')[0];
197+
assert(speccyemuframe instanceof HTMLIFrameElement);
198+
if ('contentWindow' in speccyemuframe) {
199+
const emuwindow = unwrap(speccyemuframe.contentWindow);
200+
const tmstr = Date.now();
201+
emuwindow.location =
202+
'https://static.ce-cdn.net/jsspeccy/index.html?' +
203+
tmstr +
204+
'#b64tape=' +
205+
encodeURIComponent(artifact.content);
206+
}
207+
});
208+
},
209+
},
210+
);
211+
}
212+
213+
private emulateBbcDisk(artifact: Artifact): void {
214+
const dialog = $('#jsbeebemu');
215+
216+
this.alertSystem.notify(
217+
'Click <a target="_blank" id="emulink" style="cursor:pointer;" class="link-primary">here</a> to emulate',
218+
{
219+
group: 'emulation',
220+
collapseSimilar: true,
221+
dismissTime: 10000,
222+
onBeforeShow: elem => {
223+
elem.find('#emulink').on('click', () => {
224+
BootstrapUtils.showModal(dialog);
225+
226+
const jsbeebemuframe = dialog.find('#jsbeebemuframe')[0];
227+
assert(jsbeebemuframe instanceof HTMLIFrameElement);
228+
if ('contentWindow' in jsbeebemuframe) {
229+
const emuwindow = unwrap(jsbeebemuframe.contentWindow);
230+
const tmstr = Date.now();
231+
emuwindow.location =
232+
'https://bbc.godbolt.org/?' +
233+
tmstr +
234+
'#embed&autoboot&disc1=b64data:' +
235+
encodeURIComponent(artifact.content);
236+
}
237+
});
238+
},
239+
},
240+
);
241+
}
242+
243+
private emulateNESROM(artifact: Artifact): void {
244+
const dialog = $('#jsnesemu');
245+
246+
this.alertSystem.notify(
247+
'Click <a target="_blank" id="emulink" style="cursor:pointer;" class="link-primary">here</a> to emulate',
248+
{
249+
group: 'emulation',
250+
collapseSimilar: true,
251+
dismissTime: 10000,
252+
onBeforeShow: elem => {
253+
elem.find('#emulink').on('click', () => {
254+
BootstrapUtils.showModal(dialog);
255+
256+
const jsnesemuframe = dialog.find('#jsnesemuframe')[0];
257+
assert(jsnesemuframe instanceof HTMLIFrameElement);
258+
if ('contentWindow' in jsnesemuframe) {
259+
const emuwindow = unwrap(jsnesemuframe.contentWindow);
260+
const tmstr = Date.now();
261+
emuwindow.location =
262+
'https://static.ce-cdn.net/jsnes-ceweb/index.html?' +
263+
tmstr +
264+
'#b64nes=' +
265+
encodeURIComponent(artifact.content);
266+
}
267+
});
268+
},
269+
},
270+
);
271+
}
272+
273+
private emulateC64Prg(prg: Artifact): void {
274+
this.alertSystem.notify(
275+
'Click <a target="_blank" id="emulink" style="cursor:pointer;" class="link-primary">here</a> to emulate',
276+
{
277+
group: 'emulation',
278+
collapseSimilar: true,
279+
dismissTime: 10000,
280+
onBeforeShow: elem => {
281+
elem.find('#emulink').on('click', () => {
282+
const tmstr = Date.now();
283+
const url =
284+
'https://static.ce-cdn.net/viciious/viciious.html?' +
285+
tmstr +
286+
'#filename=' +
287+
encodeURIComponent(prg.title) +
288+
'&b64c64=' +
289+
encodeURIComponent(prg.content);
290+
291+
window.open(url, '_blank');
292+
});
293+
},
294+
},
295+
);
296+
}
297+
298+
private emulateGameBoyROM(prg: Artifact): void {
299+
const dialog = $('#gbemu');
300+
301+
this.alertSystem.notify(
302+
'Click <a target="_blank" id="emulink" style="cursor:pointer;" class="link-primary">here</a> to emulate with a debugger, ' +
303+
'or <a target="_blank" id="emulink-play" style="cursor:pointer;" class="link-primary">here</a> to emulate just to play.',
304+
{
305+
group: 'emulation',
306+
collapseSimilar: true,
307+
dismissTime: 10000,
308+
onBeforeShow: elem => {
309+
elem.find('#emulink').on('click', () => {
310+
const tmstr = Date.now();
311+
const url =
312+
'https://static.ce-cdn.net/wasmboy/index.html?' +
313+
tmstr +
314+
'#rom-name=' +
315+
encodeURIComponent(prg.title) +
316+
'&rom-data=' +
317+
encodeURIComponent(prg.content);
318+
window.open(url, '_blank');
319+
});
320+
321+
elem.find('#emulink-play').on('click', () => {
322+
BootstrapUtils.showModal(dialog);
323+
324+
const gbemuframe = dialog.find('#gbemuframe')[0];
325+
assert(gbemuframe instanceof HTMLIFrameElement);
326+
if ('contentWindow' in gbemuframe) {
327+
const emuwindow = unwrap(gbemuframe.contentWindow);
328+
const tmstr = Date.now();
329+
const url =
330+
'https://static.ce-cdn.net/wasmboy/iframe/index.html?' +
331+
tmstr +
332+
'#rom-name=' +
333+
encodeURIComponent(prg.title) +
334+
'&rom-data=' +
335+
encodeURIComponent(prg.content);
336+
emuwindow.location = url;
337+
}
338+
});
339+
},
340+
},
341+
);
342+
}
343+
}

0 commit comments

Comments
 (0)