├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── bin
└── livepython
├── dist
├── images
│ ├── fastforward.png
│ ├── pause.png
│ └── play.png
├── index.html
├── main.js
├── monaco.ttf
├── style.css
├── variable_inspector.html
└── variable_inspector.js
├── livepython.icns
├── livepython.png
├── main.js
├── package.json
├── src
├── components
│ ├── CodeView.js
│ ├── MainView.js
│ └── VariableInspector.js
├── index.js
└── variable_inspector.js
├── tracer.py
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"]
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log*
3 | .npm
4 | *.lock
5 | package-lock.json
6 | .DS_Store
7 | .vs-code
8 | tags
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Anastasis Germanidis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |
4 | ## Livepython
5 | ### Watch your Python run like a movie.
6 |
7 | ##### NOTE: Livepython is alpha software. It doesn't handle a lot of edge cases and features may change.
8 |
9 | Livepython is a desktop app that lets you visually trace, in real-time, the execution of a Python program. In addition, it can track changes in global and local variables as your program is running. Livepython is meant to give you a quick grasp of a program's execution flow. It's less messy than sprinkling print statements throughout your code and simpler to use than debuggers/profilers.
10 |
11 | Livepython can be launched from the command-line as easily as:
12 |
13 | livepython [program] [args...]
14 |
15 | **Controls:**
16 |
17 | SPACE: Play/Pause the program.
18 |
19 | Left/Right Arrow: Change speed of execution.
20 |
21 | V: Open/Close Variable Inspector.
22 |
23 | ### Compatibility
24 |
25 | | **Python Version** | **Compatible?** |
26 | |-----------|---------------|
27 | | 3.6 | ✅ |
28 | | 3.5 | ✅ |
29 | | 2.7 | ✅ |
30 | | 2.6 | ❌ |
31 |
32 | ### Installation
33 |
34 | npm install livepython -g
35 |
36 | ### Development
37 |
38 |
39 |
40 | Livepython has 3 main components:
41 |
42 | * a Python [tracer](https://github.com/agermanidis/livepython/blob/master/tracer.py) that uses `sys.settrace()` to intercept every line of your program as it's being evaluated
43 | * an [Electron app](https://github.com/agermanidis/livepython/blob/master/main.js) that is responsible for the rendering the Livepython frontend
44 | * a node.js [gateway script](https://github.com/agermanidis/livepython/blob/master/bin/livepython) that manages communication between the frontend and the tracer
45 |
46 | If you want to make changes to Livepython, you will need to run [webpack](https://webpack.js.org/):
47 |
48 | webpack
49 |
50 | Then you can test your built version of livepython by running:
51 |
52 | bin/livepython [your python program]
53 |
54 | ### License
55 |
56 | MIT
57 |
--------------------------------------------------------------------------------
/bin/livepython:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require("fs");
4 | var electronPath = require("electron");
5 |
6 | const net = require("net");
7 |
8 | var buffer = [];
9 | var electronWindowOpened = false;
10 | var socket;
11 |
12 | net.createServer((s) => {
13 | socket = s;
14 |
15 | const pythonLineStream = byline.createStream(socket);
16 | pythonLineStream.on("data", line => {
17 | line = line.toString();
18 | if (!line.length) return;
19 | if (electronWindowOpened) {
20 | electronProcess.send(line);
21 | } else {
22 | buffer.push(line);
23 | }
24 | });
25 | }).listen(4387)
26 |
27 | const { spawn } = require("child_process");
28 | const byline = require("byline")
29 |
30 | var args = process.argv.slice(2)
31 |
32 | if (!args.length) {
33 | console.log("Usage: livepython [program] [..args]")
34 | process.exit()
35 | }
36 |
37 | args.unshift(__dirname + "/../tracer.py")
38 |
39 | const electronProcess = spawn(electronPath, [__dirname + "/../"], {
40 | stdio: ["pipe", "pipe", "pipe", "ipc"]
41 | })
42 |
43 | const pythonProcess = spawn("python", args);
44 |
45 | pythonProcess.stdout.on("data", data => {
46 | process.stdout.write(data.toString());
47 | });
48 |
49 | pythonProcess.stderr.on("data", data => {
50 | process.stdout.write(data.toString())
51 | })
52 |
53 | pythonProcess.on("exit", (code) => {
54 | electronProcess.kill('SIGINT')
55 | process.exit();
56 | })
57 |
58 | electronProcess.on("message", msg => {
59 | if (msg.type === 'connected') {
60 | electronWindowOpened = true;
61 | buffer.forEach(msg => {
62 | electronProcess.send(msg);
63 | });
64 | } else if (msg.type === "toggle_running_state") {
65 | if (msg.value) {
66 | pythonProcess.kill("SIGSTOP")
67 | } else {
68 | pythonProcess.kill("SIGCONT");
69 | }
70 | } else {
71 | socket.write(JSON.stringify(msg))
72 | }
73 | })
74 |
75 | function killSubprocesses (e) {
76 | electronProcess.kill("SIGINT");
77 | pythonProcess.kill("SIGINT");
78 | process.exit();
79 | }
80 |
81 | process.on('exit', killSubprocesses)
82 | process.on('SIGINT', killSubprocesses)
83 | process.on("SIGUSR1", killSubprocesses)
84 | process.on("SIGUSR2", killSubprocesses)
85 | process.on("uncaughtException", killSubprocesses)
--------------------------------------------------------------------------------
/dist/images/fastforward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/dist/images/fastforward.png
--------------------------------------------------------------------------------
/dist/images/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/dist/images/pause.png
--------------------------------------------------------------------------------
/dist/images/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/dist/images/play.png
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | livepython
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/dist/monaco.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/dist/monaco.ttf
--------------------------------------------------------------------------------
/dist/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: Monaco;
3 | src: url(monaco.ttf)
4 | }
5 |
6 | body {
7 | font-family: Monaco;
8 | color: white;
9 | background: #08091D;
10 | margin: 0;
11 | }
12 |
13 | #content {
14 | position: fixed;
15 | height: 100%;
16 | width: 100%;
17 | }
18 |
19 | #main {
20 | display: flex;
21 | flex-direction: column;
22 | flex-wrap: nowrap;
23 | height: 100%;
24 | }
25 |
26 | #header-view {
27 | flex: 0 0 50px;
28 | }
29 |
30 | #main-area {
31 | overflow: scroll;
32 | }
33 |
34 | .line {
35 | background: rgba(0, 0, 0, 0);
36 |
37 | }
38 |
39 | .selected-line {
40 | background: #278865;
41 |
42 | }
43 |
44 | .selected-paused {
45 | background: #717171;
46 | }
47 |
48 | .error {
49 | background: #AD4343;
50 | }
51 |
52 | .error-msg {
53 | display: inline;
54 | font-size: 20px;
55 | padding: 10px;
56 | }
57 |
58 | #program-title {
59 | padding: 10px;
60 | text-align: center;
61 | margin: 0px;
62 | color: white;
63 | background-color: #278865;
64 | }
65 |
66 | #code-view {
67 | background: none;
68 | font-size: 18px;
69 | outline: none;
70 | flex: auto;
71 | }
72 |
73 | #source pre {
74 | margin: 0;
75 | /* margin-left: 35px; */
76 | }
77 |
78 | #source pre code {
79 | background: none;
80 | color: #c5c8c6;
81 | margin-left: 10px;
82 | }
83 |
84 | #line-numbers {
85 | margin-top: 10px;
86 | width: 50px;
87 | font-size: 15px;
88 | float: left;
89 | }
90 |
91 | .line {
92 | padding: 5px;
93 | margin: 0;
94 | /* transition-duration: 0.05s; */
95 | font-size: 15px;
96 | font-family: Monaco;
97 | }
98 |
99 | .line-number {
100 | color: #AAA;
101 | padding: 0 5px;
102 | margin: 0;
103 | float: left;
104 | }
105 |
106 | #main-area {
107 | height: 100%;
108 | transition-duration: 1s;
109 | display: flex;
110 | flex-direction: row;
111 | }
112 |
113 | .finished {
114 | background-color: #131F1D;
115 | }
116 |
117 | .failed {
118 | background-color: #4C0F19;
119 | }
120 |
121 | .finished #program-title {
122 | background-color: #278865;
123 | }
124 |
125 | .paused #program-title {
126 | background-color: #6B6B6B;
127 | }
128 |
129 | .failed #program-title {
130 | background-color: red;
131 | }
132 |
133 | pre#exception-message {
134 | font-size: 13px;
135 | padding-left: 20px;
136 | color: white;
137 | margin: 15px;
138 | font-family: Monaco;
139 | }
140 |
141 | .exception {
142 | background: #820c0c;
143 | }
144 |
145 | .exception-message .hljs-string {
146 | color: white;
147 | }
148 |
149 | .exception-message .hljs-keyword {
150 | color: white;
151 | }
152 |
153 | #program-state {
154 | font-size: 13px;
155 | padding: 5px;
156 | border-radius: 5px;
157 | margin: 15px;
158 | height: 15px;
159 | }
160 |
161 | #program-state.running {
162 | background: green;
163 | }
164 |
165 | #program-state.paused {
166 | background: gray;
167 | }
168 |
169 | code {
170 | font-family: Monaco;
171 | }
172 |
173 | #code-area {
174 | padding-top: 10px;
175 | padding-left: 5px;
176 | }
177 |
178 | .status-indicator {
179 | position: fixed;
180 | top: 50%;
181 | left: 50%;
182 | width: 150px;
183 | height: 150px;
184 | margin-top: -75px;
185 | margin-left: -75px;
186 | }
187 |
188 | #variables-view {
189 | font-family: Helvetica Neue;
190 | }
191 |
192 | #variable-search {
193 | background: none;
194 | outline: none;
195 | font-size: 20px;
196 | padding: 10px;
197 | border: none;
198 | border-left: 5px solid white;
199 | margin-left: 20px;
200 | margin: 10px;
201 | color: white;
202 | }
203 |
204 | table {
205 |
206 | width: 100%;
207 | margin-top: 10px;
208 | font-size: 16px;
209 | padding: 0 5px;
210 |
211 | }
212 |
213 | thead {
214 | background: #353434;
215 | font-weight: bold;
216 | }
217 |
218 | /* tbody tr:nth-child(even) {
219 | background: rgba(255, 255, 255, 0.2);
220 | }
221 | */
222 | td {
223 | padding: 10px;
224 | }
--------------------------------------------------------------------------------
/dist/variable_inspector.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Variable Inspector
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/livepython.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/livepython.icns
--------------------------------------------------------------------------------
/livepython.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/livepython.png
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const {app, BrowserWindow, ipcMain} = require('electron')
2 | const path = require('path')
3 | const url = require('url')
4 |
5 | let win
6 | let varInspector;
7 |
8 | function openVariableInspector () {
9 | varInspector = new BrowserWindow({
10 | x: 800,
11 | y: 0,
12 | width: 500,
13 | height: 800,
14 | title: "Variable Inspector"
15 | });
16 |
17 | varInspector.loadURL(url.format({
18 | pathname: path.join(__dirname, "dist", "variable_inspector.html"),
19 | protocol: "file:",
20 | slashes: true
21 | }));
22 |
23 | varInspector.on('close', () => {
24 | varInspector = null;
25 | })
26 | }
27 |
28 | function createWindow () {
29 | win = new BrowserWindow({
30 | x: 20,
31 | y: 0,
32 | width: 750,
33 | height: 1000,
34 | icon: path.join(__dirname, 'livepython.png'),
35 | title: "Livepython"
36 | })
37 |
38 | win.loadURL(url.format({
39 | pathname: path.join(__dirname, 'dist', 'index.html'),
40 | protocol: 'file:',
41 | slashes: true,
42 | }))
43 |
44 | ipcMain.on("command", (evt, msg) => {
45 | process.send(msg)
46 | })
47 |
48 | ipcMain.on("toggle_variable_inspector", (evt, msg) => {
49 | if (varInspector) {
50 | varInspector.close();
51 | varInspector = null;
52 | } else {
53 | openVariableInspector();
54 | }
55 | });
56 |
57 | process.on('message', message => {
58 | const parsed = JSON.parse(message)
59 | if (parsed.type === 'finish') {
60 | app.quit()
61 | }
62 | if (win) win.webContents.send('trace', { msg: message })
63 | if (varInspector) varInspector.webContents.send('trace', { msg: message });
64 | })
65 |
66 | // win.webContents.openDevTools()
67 |
68 | win.on('closed', () => {
69 | win = null
70 | })
71 | }
72 |
73 | app.on('ready', createWindow)
74 |
75 | app.on('window-all-closed', () => {
76 | app.quit()
77 | })
78 |
79 | app.on('activate', () => {
80 | if (win === null) {
81 | createWindow()
82 | }
83 | })
84 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "livepython",
3 | "description": "Visually trace your Python program as it's running",
4 | "version": "0.0.6",
5 | "main": "main.js",
6 | "bin": {
7 | "livepython": "./bin/livepython"
8 | },
9 | "icon": "livepython.png",
10 | "license": "MIT",
11 | "dependencies": {
12 | "byline": "^5.0.0",
13 | "chroma-js": "^1.3.4",
14 | "electron": "1.7.8",
15 | "electron-packager": "^9.1.0",
16 | "jquery": "^3.2.1",
17 | "mousetrap": "^1.6.1",
18 | "process-nextick-args": "^1.0.7",
19 | "react": "^16.0.0",
20 | "react-dom": "^16.0.0",
21 | "react-syntax-highlighter": "^5.7.0",
22 | "util-deprecate": "^1.0.2"
23 | },
24 | "devDependencies": {
25 | "babel-cli": "^6.26.0",
26 | "babel-core": "^6.26.0",
27 | "babel-loader": "^7.1.2",
28 | "babel-preset-env": "^1.6.0",
29 | "babel-preset-es2015": "^6.24.1",
30 | "babel-preset-react": "^6.24.1",
31 | "webpack": "^3.6.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/CodeView.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import jq from 'jquery'
3 |
4 | import SyntaxHighlighter from 'react-syntax-highlighter'
5 | import { tomorrowNight } from 'react-syntax-highlighter/dist/styles'
6 |
7 | tomorrowNight.hljs.background = 'none'
8 | tomorrowNight.hljs.padding = 0
9 |
10 | class CodeView extends Component {
11 | constructor() {
12 | super();
13 | this.state = {
14 | activity: null,
15 | intervalId: null
16 | };
17 | }
18 |
19 | spaceFixedLineNumber(curLine, totLines) {
20 | return (
21 | "\u00a0".repeat(totLines.toString().length - curLine.toString().length) +
22 | curLine.toString()
23 | );
24 | }
25 |
26 | componentDidMount() {
27 | this.tick();
28 | }
29 |
30 | tick() {
31 | try {
32 | const currentOffset = document.scrollingElement.scrollTop;
33 | const targetOffset = jq(".selected").offset().top - 400;
34 | const change = (targetOffset - currentOffset) / 30;
35 | document.scrollingElement.scrollTop += change;
36 | } catch (e) {}
37 | requestAnimationFrame(this.tick.bind(this));
38 | }
39 |
40 | componentWillReceiveProps(props) {
41 | var activity = JSON.parse(JSON.stringify(this.state.activity)) || {};
42 |
43 | if (!hasOwnProperty.call(activity, props.filename)) {
44 | activity[props.filename] = {};
45 | }
46 |
47 | activity[props.filename][props.lineno - 1] = Date.now();
48 |
49 | this.setState({ activity });
50 | }
51 |
52 | render() {
53 | if (!this.state.activity) return "";
54 | if (!this.state.activity[this.props.filename]) return "";
55 | if (!this.props.source) return "";
56 | var lines = this.props.source.split("\n");
57 | var lineEls = [];
58 | for (var i = 0; i < lines.length; i++) {
59 | var line = lines[i];
60 | if (!line.length) line = "\n";
61 | var isExceptionLine = false;
62 | var cs = "line";
63 | var exceptionMessage;
64 | if (this.props.exception) {
65 | var exception = this.props.exception;
66 | if (i === exception.lineno - 1) {
67 | isExceptionLine = true;
68 | cs += " exception";
69 | exceptionMessage = " " + exception.type + ": " + exception.message;
70 | }
71 | } else if (
72 | i === this.props.lineno - 1 &&
73 | this.props.state !== "finished"
74 | ) {
75 | cs += " selected";
76 | }
77 | const lastActive = this.state.activity[this.props.filename][i] || 0;
78 | const opacity = 1 - Math.min(1, (Date.now() - lastActive) / 800);
79 | var el = (
80 |
87 |
88 | {this.spaceFixedLineNumber(i + 1, lines.length)}
89 |
90 |
91 | {line}
92 |
93 | {isExceptionLine && (
94 |
{exceptionMessage}
95 | )}
96 | {"\n"}
97 |
98 | );
99 | lineEls.push(el);
100 | }
101 | return (
102 |
107 | );
108 | }
109 | }
110 |
111 | export default CodeView
112 |
--------------------------------------------------------------------------------
/src/components/MainView.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { ipcRenderer } from 'electron'
3 | import jq from 'jquery'
4 | import mousetrap from 'mousetrap'
5 |
6 | import CodeView from './CodeView'
7 |
8 | function showIndicator(name) {
9 | jq(".status-indicator").remove();
10 | const src = `images/${name}.png`
11 | var $img = jq("
");
12 | $img.attr("src", src);
13 | $img.addClass("status-indicator");
14 | jq("body").append($img);
15 | $img.fadeOut(1000, function() {
16 | $img.remove();
17 | });
18 | }
19 |
20 | class MainView extends Component {
21 | constructor () {
22 | super()
23 | this.state = {
24 | paused: false,
25 | state: "running",
26 | filename: null,
27 | function_name: null,
28 | lineno: 0,
29 | locals: {},
30 | source: "",
31 | exception: null,
32 | time: null,
33 | fastForward: false
34 | }
35 | }
36 |
37 | componentDidUpdate () {
38 | document.title = 'Livepython - ' + this.state.filename;
39 | }
40 |
41 | componentWillMount () {
42 | ipcRenderer.send('command', {
43 | type: 'connected'
44 | })
45 | ipcRenderer.on('trace', (event, data) => {
46 | const msg = JSON.parse(data.msg)
47 | if (msg.type === 'call') {
48 | this.setState(Object.assign(this.state, msg))
49 | } else if (msg.type === 'switch') {
50 | this.setState(Object.assign(this.state, msg));
51 | } else if (msg.type === 'finish') {
52 | this.setState({state: 'finished'})
53 | } else if (msg.type === 'exception') {
54 | this.setState({
55 | state: 'failed',
56 | exception: {
57 | message: msg.exception_message,
58 | type: msg.exception_type,
59 | lineno: msg.lineno
60 | }
61 | })
62 | }
63 | })
64 |
65 | mousetrap.bind("space", (evt) => {
66 | const {paused} = this.state;
67 | showIndicator(paused ? 'play' : 'pause');
68 | ipcRenderer.send('command', {
69 | type: "toggle_running_state",
70 | value: !paused
71 | });
72 | this.setState({paused: !this.state.paused})
73 | return false;
74 | });
75 |
76 | mousetrap.bind("right", evt => {
77 | showIndicator('fastforward');
78 | ipcRenderer.send('command', {
79 | type: 'change_speed',
80 | speed: 'fast'
81 | });
82 | this.setState({ fastForward: true });
83 | return false;
84 | });
85 |
86 | mousetrap.bind("left", evt => {
87 | showIndicator("play");
88 | ipcRenderer.send('command', {
89 | type: "change_speed",
90 | speed: "slow"
91 | });
92 | this.setState({ fastForward: false });
93 | return false;
94 | });
95 |
96 | mousetrap.bind("v", evt => {
97 | ipcRenderer.send("toggle_variable_inspector");
98 | return false;
99 | });
100 | }
101 |
102 |
103 | render () {
104 | if (!this.state.source) return Loading
105 | return (
106 |
107 |
108 |
109 | )
110 | }
111 | }
112 |
113 | export default MainView
--------------------------------------------------------------------------------
/src/components/VariableInspector.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { ipcRenderer } from "electron";
3 |
4 | function truncateString(str, num) {
5 | if (num < str.length) {
6 | return str.slice(0, num) + "...";
7 | } else {
8 | return str;
9 | }
10 | }
11 |
12 | function arrDiff (a1, a2) {
13 | var a = [], diff = [];
14 |
15 | for (var i = 0; i < a1.length; i++) {
16 | a[a1[i]] = true;
17 | }
18 | for (var i = 0; i < a2.length; i++) {
19 | if (a[a2[i]]) {
20 | delete a[a2[i]];
21 | } else {
22 | a[a2[i]] = true;
23 | }
24 | }
25 | for (var k in a) {
26 | diff.push(k);
27 | }
28 | return diff;
29 | };
30 |
31 | class VariableInspector extends Component {
32 | constructor() {
33 | super();
34 | this.state = {
35 | filter: "",
36 | hideModules: true,
37 | variables: {},
38 | recentlyChanged: {}
39 | };
40 | }
41 |
42 | componentWillMount () {
43 | ipcRenderer.send('command', {
44 | type: 'connected'
45 | })
46 | ipcRenderer.on('trace', (event, data) => {
47 | const msg = JSON.parse(data.msg)
48 | if (msg.type === 'call') {
49 | this.setState({
50 | variables: Object.assign(
51 | msg.frame_locals,
52 | msg.frame_globals
53 | ),
54 | prevVariables: Object.assign(this.state.variables),
55 | recentlyChanged: arrDiff(
56 | Object.keys(msg.frame_locals).concat(Object.keys(msg.frame_globals)),
57 | Object.keys(this.state.variables)
58 | )
59 | });
60 | }
61 | })
62 | }
63 |
64 | getKeys() {
65 | var keys = Object.keys(this.state.variables);
66 | var filtered = [];
67 | for (var i = 0; i < keys.length; i++) {
68 | var name = keys[i];
69 | var type = this.state.variables[name].type;
70 | if (this.state.hideModules && (type === "module" ||
71 | type === "function" ||
72 | type === "classobj" ||
73 | type === '_Feature' ||
74 | type === 'type')) {
75 | continue;
76 | }
77 | if (!this.state.filter || keys[i].startsWith(this.state.filter)) {
78 | filtered.push(keys[i]);
79 | }
80 | }
81 | return filtered;
82 | }
83 |
84 | filterChange (evt) {
85 | this.setState({ filter: evt.target.value || null });
86 | }
87 |
88 | toggleModuleHide() {
89 | this.setState({ hideModules: !this.state.hideModules });
90 | }
91 |
92 | componentDidMount() {
93 | this.searchInput.focus();
94 | }
95 |
96 | render() {
97 | var keys = this.getKeys();
98 | var rows = [];
99 | for (var i = 0; i < keys.length; i++) {
100 | var name = keys[i];
101 | var type = this.state.variables[name].type;
102 | var value = this.state.variables[name].value;
103 | var cs = 'variable-line'
104 | console.log(this.state.prevVariables[name], this.state.variables[name]);
105 | if (!this.state.prevVariables[name] || this.state.prevVariables[name].value !== this.state.variables[name].value) {
106 | cs += " selected-line";
107 | }
108 | console.log(value, typeof value)
109 | rows.push(
110 | {name} |
111 | {type} |
112 | {truncateString(value.toString(), 30)} |
113 |
);
114 | }
115 | return ;
136 | }
137 | }
138 |
139 | export default VariableInspector;
140 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | import MainView from './components/MainView'
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById('root')
9 | )
10 |
--------------------------------------------------------------------------------
/src/variable_inspector.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import VariableInspector from "./components/VariableInspector";
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById("root")
9 | );
10 |
--------------------------------------------------------------------------------
/tracer.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import json
4 | import linecache
5 | import os
6 | import sys
7 | import threading
8 | import inspect
9 | import time
10 | import socket
11 |
12 | state = {
13 | 'speed': 'slow'
14 | }
15 |
16 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
17 | s.connect(("localhost", 4387))
18 | s.setblocking(False)
19 |
20 | def debounce(wait):
21 | def decorator(fn):
22 | class context:
23 | last_call = None
24 | def debounced(*args, **kwargs):
25 | def call_it():
26 | args, kwargs = context.last_call
27 | fn(*args, **kwargs)
28 | context.last_call = None
29 | if context.last_call is None:
30 | debounced.t = threading.Timer(wait, call_it)
31 | debounced.t.start()
32 | context.last_call = (args, kwargs)
33 | return debounced
34 | return decorator
35 |
36 |
37 | def log(msg):
38 | try:
39 | s.send(bytes(msg+'\n', 'utf8'))
40 | except:
41 | s.send(msg+'\n')
42 |
43 |
44 | @debounce(0.1)
45 | def log_frame(frame):
46 | log(json.dumps(generate_call_event(frame)))
47 |
48 |
49 | starting_filename = os.path.abspath(sys.argv[1])
50 | starting_dir = os.path.dirname(starting_filename)
51 |
52 | os.chdir(starting_dir)
53 | sys.path.insert(0, starting_dir)
54 |
55 | current_filename = None
56 | current_line = None
57 | current_locals = {}
58 | failed = False
59 |
60 |
61 | def should_ignore_variable(name):
62 | return name.startswith('__') and name.endswith('__')
63 |
64 |
65 | def truncate_list(l):
66 | if len(l) > 3:
67 | ret = ', '.join(map(process_variable, l[:2]))
68 | ret += ", ..., "
69 | ret += process_variable(l[-1])
70 | return ret
71 | else:
72 | return ', '.join(map(process_variable, l))
73 |
74 |
75 | def format_function(f):
76 | args = inspect.getargspec(f).args
77 | return "function(%s)" % truncate_list(args)
78 |
79 |
80 | def format_list(l):
81 | return "[%s]" % truncate_list(l)
82 |
83 |
84 | def process_variable(var):
85 | type_name = type(var).__name__
86 | if type_name == 'list':
87 | return format_list(var)
88 | elif type_name == 'module':
89 | return "" % var.__name__
90 | else:
91 | return str(var)
92 |
93 |
94 | def get_module_name(full_path):
95 | global starting_filename
96 | return os.path.relpath(
97 | os.path.abspath(full_path),
98 | os.path.dirname(os.path.abspath(starting_filename))
99 | )
100 |
101 |
102 | def generate_call_event(frame):
103 | frame_locals = {k:
104 | {'value': process_variable(v), 'type': type(v).__name__}
105 | for k, v in frame.f_locals.items() if not should_ignore_variable(k)
106 | }
107 | frame_globals = {k:
108 | {'value': process_variable(v), 'type': type(v).__name__}
109 | for k, v in frame.f_globals.items() if not should_ignore_variable(k)
110 | }
111 | obj = {
112 | 'type': 'call',
113 | 'frame_locals': frame_locals,
114 | 'frame_globals': frame_globals,
115 | 'filename': get_module_name(frame.f_code.co_filename),
116 | 'lineno': frame.f_lineno,
117 | 'source': ''.join(linecache.getlines(frame.f_code.co_filename))
118 | }
119 | return obj
120 |
121 |
122 | def generate_exception_event(e):
123 | return {
124 | 'type': 'exception',
125 | 'exception_type': type(e).__name__,
126 | 'exception_message': str(e),
127 | 'filename': current_filename,
128 | 'lineno': current_line,
129 | 'time': time.time()
130 | }
131 |
132 |
133 | def process_msg(msg):
134 | global state
135 | if type(msg) == bytes:
136 | msg = msg.decode('utf8')
137 | msg = json.loads(msg)
138 | if msg['type'] == 'change_speed':
139 | print('changed speed')
140 | state['speed'] = msg['speed']
141 |
142 |
143 | def local_trace(frame, why, arg):
144 | try:
145 | received_msg = s.recv(1024)
146 | process_msg(received_msg)
147 | except:
148 | pass
149 |
150 | global current_line
151 | global current_filename
152 |
153 | if failed:
154 | return
155 |
156 | if why == 'exception':
157 | exc_type = arg[0].__name__
158 | exc_msg = arg[1]
159 | return
160 |
161 | current_filename = frame.f_code.co_filename
162 | current_line = frame.f_lineno
163 |
164 | if not current_filename.startswith(starting_dir):
165 | return
166 |
167 | if 'livepython' in current_filename:
168 | return
169 |
170 | if 'site-packages' in current_filename:
171 | return
172 |
173 | if 'lib/python' in current_filename:
174 | return
175 |
176 | log_frame(frame)
177 |
178 | if state['speed'] == 'slow':
179 | time.sleep(1)
180 |
181 | return local_trace
182 |
183 |
184 | def global_trace(frame, why, arg):
185 | return local_trace
186 |
187 |
188 | with open(starting_filename, 'rb') as fp:
189 | code = compile(fp.read(), starting_filename, 'exec')
190 |
191 |
192 | namespace = {
193 | '__file__': starting_filename,
194 | '__name__': '__main__',
195 | }
196 |
197 |
198 | log(json.dumps({
199 | 'type': 'start',
200 | 'startmodule': starting_filename
201 | }))
202 |
203 | sys.settrace(global_trace)
204 | threading.settrace(global_trace)
205 |
206 | try:
207 | sys.argv = sys.argv[1:]
208 | exec(code, namespace)
209 | log(json.dumps({'type': 'finish'}))
210 | except Exception as err:
211 | failed = True
212 | log(json.dumps(generate_exception_event(err)))
213 | finally:
214 | sys.settrace(None)
215 | threading.settrace(None)
216 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | entry: {
5 | main: './src/index.js',
6 | variable_inspector: './src/variable_inspector.js'
7 | },
8 | target: 'electron',
9 | module: {
10 | loaders: [
11 | { test: /\.js$/, loader: 'babel-loader', exclude: /(node_modules|main)/ }
12 | ]
13 | },
14 | output: {
15 | filename: '[name].js',
16 | path: path.resolve(__dirname, 'dist')
17 | },
18 | }
19 |
--------------------------------------------------------------------------------