Skip to content

Commit fad79c4

Browse files
committed
run python in worker
1 parent b7a3016 commit fad79c4

12 files changed

Lines changed: 283 additions & 42 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Plugin to run python code in [Acode](https://acode.foxdebug.com) editor for andr
44

55
## Plugin is underdevelopment
66

7-
Using this plugin you can run python codes but doesn't output error yet, so, it might be difficult to debug your python code.
7+
Using this plugin you can run python codes but you cannot import other python files yet, so, this feature will be supportted in future updates.
88

99
## WARNING
1010

dist/lib/index.html

Lines changed: 0 additions & 14 deletions
This file was deleted.

dist/main.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*!*******************************************************************************************************************************************************************************!*\
2+
!*** css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[2].use[1]!./node_modules/postcss-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/style.scss ***!
3+
\*******************************************************************************************************************************************************************************/
4+
#acode-plugin-python textarea {
5+
background-color: transparent;
6+
color: inherit;
7+
width: 100%; }
8+

dist/main.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/worker.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/worker.js.LICENSE.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"author": "Ajit <me@ajitkumar.dev>",
88
"license": "MIT",
99
"dependencies": {
10-
"html-tag-js": "^1.0.4"
10+
"autoprefixer": "^10.4.7",
11+
"html-tag-js": "^1.0.4",
12+
"sass-loader": "^13.0.0"
1113
},
1214
"devDependencies": {
1315
"@babel/cli": "^7.17.10",
@@ -26,4 +28,4 @@
2628
"scripts": {
2729
"build": "webpack"
2830
}
29-
}
31+
}

plugin.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
"id": "acode.plugin.python",
33
"name": "Python",
44
"main": "dist/main.js",
5-
"version": "1.0.0",
5+
"version": "1.0.1",
66
"readme": "README.md",
77
"icon": "icon.png",
88
"type": "pro",
99
"files": [
10+
"worker.js",
1011
"lib/distutils.tar",
1112
"lib/micropip-0.1-py3-none-any.whl",
1213
"lib/packages.json",

src/main.js

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
import tag from 'html-tag-js';
22

33
class Python {
4+
#worker;
5+
#onInitError;
6+
#onInitSuccess;
7+
#onRunSuccess;
8+
#onRunError;
9+
#cacheFile;
410
name = 'Python';
511
baseUrl = '';
612
pyodide = null;
713
$input = null;
814
$page = null;
915
$runBtn = null;
10-
async init($page) {
16+
17+
async init($page, cacheFile, cacheFileUrl) {
18+
if (window.toast) {
19+
window.toast('Python is loading...');
20+
}
21+
$page.id = 'acode-plugin-python';
1122
this.$page = $page;
1223
this.$page.settitle('Python');
24+
this.#cacheFile = cacheFile;
25+
const onhide = $page.onhide;
26+
$page.onhide = () => {
27+
this.#cacheFile.writeFile('\0');
28+
onhide();
29+
}
1330

1431
let main = this.$page.get('.main');
1532

@@ -21,18 +38,18 @@ class Python {
2138
main.style.padding = '10px';
2239
main.style.overflow = 'auto';
2340
main.style.boxSizing = 'border-box';
24-
25-
const $script = tag('script', {
26-
src: this.baseUrl + 'lib/pyodide.js',
41+
this.#worker = new Worker(this.baseUrl + 'worker.js');
42+
console.log('worker line', this.#worker.line);
43+
this.#worker.postMessage({
44+
action: 'init',
45+
baseUrl: this.baseUrl,
46+
cacheFileUrl,
2747
});
48+
this.#worker.onmessage = this.#workerOnMessage.bind(this);
2849

29-
30-
document.head.appendChild($script);
31-
await new Promise(resolve => $script.onload = resolve);
32-
this.pyodide = await loadPyodide({
33-
stdout: (msg) => this.print(msg),
34-
stderr: (err) => this.print(err, 'error'),
35-
stdin: () => this.read(),
50+
await new Promise((resolve, error) => {
51+
this.#onInitSuccess = resolve;
52+
this.#onInitError = error;
3653
});
3754

3855
this.$runBtn = tag('span', {
@@ -43,25 +60,53 @@ class Python {
4360
onclick: this.run.bind(this),
4461
});
4562

63+
this.$input = tag('textarea', {
64+
onkeydown: this.#onkeydown.bind(this),
65+
style: {
66+
backgroundColor: 'transparent',
67+
color: 'inherit',
68+
width: '100%',
69+
border: 'none',
70+
},
71+
});
4672
this.checkRunnable();
4773
editorManager.on('switch-file', this.checkRunnable.bind(this));
4874
editorManager.on('rename-file', this.checkRunnable.bind(this));
75+
if (window.toast) {
76+
window.toast('Python is loaded.');
77+
}
4978
}
50-
run() {
79+
80+
async run() {
81+
await this.#cacheFile.writeFile('');
5182
this.$page.get('.main').innerHTML = '';
83+
this.#append(this.$input);
5284
this.$page.classList.remove('hide');
5385
this.$page.show();
5486
setTimeout(async () => {
5587
const code = editorManager.editor.getValue();
56-
const output = await this.pyodide.runPythonAsync(code);
57-
this.print(output);
88+
this.#worker.postMessage({
89+
action: 'run',
90+
code,
91+
});
92+
try {
93+
const res = await new Promise((resolve, error) => {
94+
this.#onRunSuccess = resolve;
95+
this.#onRunError = error;
96+
});
97+
this.print(res, 'output');
98+
} catch (error) {
99+
this.print(error, 'error');
100+
}
58101
}, 600);
59102
}
103+
60104
destroy() {
61105
if (this.$runBtn) {
62106
this.$runBtn.onclick = null;
63107
this.$runBtn.remove();
64108
}
109+
this.#worker.terminate();
65110
editorManager.off('switch-file', this.checkRunnable.bind(this));
66111
editorManager.off('rename-file', this.checkRunnable.bind(this));
67112
}
@@ -75,8 +120,8 @@ class Python {
75120

76121
if (file.name.endsWith('.py')) {
77122
const $header = tag.get('header');
78-
const $editIcon = $header.get('.icon.edit');
79-
$header.insertBefore(this.$runBtn, $editIcon);
123+
$header.get('.icon.play_arrow')?.remove();
124+
$header.insertBefore(this.$runBtn, $header.lastChild);
80125
}
81126
}
82127

@@ -87,26 +132,73 @@ class Python {
87132
});
88133
$output.appendChild(tag('pre', {
89134
textContent: res,
90-
className: type
135+
style: {
136+
color: type === 'error' ? 'orangered' : 'inherit',
137+
}
91138
}));
139+
this.#append($output, this.$input);
140+
}
141+
142+
#append(...$el) {
92143
const $main = this.$page.get('.main');
93144
if (!$main) this.$page.append(tag('div', { className: 'main' }));
94-
this.$page.get('.main').append($output);
145+
this.$page.get('.main').append(...$el);
95146
}
96147

97-
read() {
98-
return prompt('(Python Input)>>>') || '';
148+
async #workerOnMessage(e) {
149+
const {
150+
action,
151+
success,
152+
error,
153+
text,
154+
} = e.data;
155+
if (action === 'init') {
156+
if (success) {
157+
this.#onInitSuccess();
158+
} else {
159+
this.#onInitError(error);
160+
}
161+
}
162+
if (action === 'run') {
163+
if (success) {
164+
this.#onRunSuccess();
165+
} else {
166+
this.#onRunError(error);
167+
}
168+
}
169+
if (action === 'input') {
170+
if (text) this.print(text);
171+
await this.#cacheFile.writeFile('');
172+
this.$input.focus();
173+
}
174+
if (action === 'stdout') {
175+
this.print(text);
176+
}
177+
if (action === 'stderr') {
178+
this.print(text, 'error');
179+
}
180+
}
181+
182+
#onkeydown(e) {
183+
if (e.key === 'Enter') {
184+
e.preventDefault();
185+
const value = this.$input.value;
186+
this.print(value);
187+
this.#cacheFile.writeFile(value);
188+
e.target.value = '';
189+
}
99190
}
100191
}
101192

193+
102194
console.log('Python plugin');
103195

104196
if (window.acode) {
105197
const python = new Python();
106-
acode.setPluginInit('acode.plugin.python', (baseUrl, $page) => {
198+
acode.setPluginInit('acode.plugin.python', (baseUrl, $page, { cacheFileUrl, cacheFile }) => {
107199
if (!baseUrl.endsWith('/')) baseUrl += '/';
108200
python.baseUrl = baseUrl;
109-
python.init($page);
201+
python.init($page, cacheFile, cacheFileUrl);
110202
console.log('Python plugin initialized');
111203
});
112204
acode.setPluginUnmount('acode.plugin.python', () => {

src/worker.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
async function loadPyodideAndPackages(baseUrl = '', packages) {
2+
importScripts(`${baseUrl}/lib/pyodide.js`);
3+
self.pyodide = await loadPyodide({
4+
stdout: (msg) => { stdout(msg) },
5+
stderr: (msg) => { stdout(msg) },
6+
stdin: () => { return stdin() },
7+
});
8+
if (Array.isArray(packages)) {
9+
await self.pyodide.installPackages(packages);
10+
}
11+
}
12+
13+
const actions = {
14+
async init(data) {
15+
const { packages, baseUrl, cacheFileUrl } = data;
16+
self.cacheFileUrl = cacheFileUrl;
17+
try {
18+
await loadPyodideAndPackages(baseUrl, packages);
19+
20+
// override python input
21+
await self.pyodide.runPython(`import sys
22+
def input(prompt=''):
23+
print(prompt)
24+
return sys.stdin.readline()
25+
26+
__builtins__.input = input
27+
`);
28+
29+
self.postMessage({
30+
action: 'init',
31+
success: true,
32+
});
33+
} catch (error) {
34+
self.postMessage({
35+
action: 'init',
36+
success: false,
37+
error,
38+
});
39+
}
40+
},
41+
async run(data) {
42+
const { code } = data;
43+
try {
44+
await self.pyodide.loadPackagesFromImports(code);
45+
const output = await self.pyodide.runPythonAsync(code);
46+
self.postMessage({
47+
action: 'run',
48+
success: true,
49+
output,
50+
});
51+
} catch (error) {
52+
self.postMessage({
53+
action: 'run',
54+
success: false,
55+
error,
56+
});
57+
}
58+
},
59+
input(data) {
60+
const { line } = data;
61+
self.line = line;
62+
},
63+
}
64+
65+
self.onmessage = async function (e) {
66+
const {
67+
action
68+
} = e.data;
69+
70+
if (actions[action]) {
71+
await actions[action](e.data);
72+
}
73+
};
74+
75+
self.line = '';
76+
77+
function stdout(msg) {
78+
self.postMessage({
79+
action: 'stdout',
80+
text: msg,
81+
});
82+
}
83+
84+
function stderr(msg) {
85+
self.postMessage({
86+
action: 'stderr',
87+
text: msg,
88+
});
89+
}
90+
91+
function stdin(str = '') {
92+
self.postMessage({
93+
action: 'input',
94+
text: str,
95+
});
96+
let line = '';
97+
while (!line) {
98+
line = read();
99+
}
100+
return line.replace('\0', '');
101+
}
102+
103+
function read() {
104+
const xhr = new XMLHttpRequest();
105+
xhr.timeout = 1000;
106+
xhr.open('GET', self.cacheFileUrl, false);
107+
try {
108+
xhr.send();
109+
return xhr.responseText;
110+
} catch (err) {
111+
throw err instanceof Error ? err : new Error(err);
112+
}
113+
}

0 commit comments

Comments
 (0)