Merge branch 'dev' into users/t-famoun/temperature_sensor
This commit is contained in:
Коммит
b7fd676457
17
PRIVACY.md
17
PRIVACY.md
|
@ -4,6 +4,19 @@
|
|||
|
||||
The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.
|
||||
|
||||
## Disable telemetry
|
||||
## Disable Telemetry
|
||||
|
||||
- [VS Code documentation to turn off telemetry for extensions](https://code.visualstudio.com/docs/getstarted/telemetry#_extensions-and-telemetry)
|
||||
The Microsoft Pacifica Extension for Visual Studio Code collects usage
|
||||
data and sends it to Microsoft to help improve our products and
|
||||
services. Read our
|
||||
[privacy statement](https://privacy.microsoft.com/privacystatement) to
|
||||
learn more. This extension respects the `telemetry.enableTelemetry`
|
||||
setting which you can learn more about at
|
||||
https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting.
|
||||
|
||||
To disable telemetry, follow these steps:
|
||||
1) Open **File** (Open **Code** on macOS)
|
||||
2) Select **Preferences**
|
||||
3) Select **Settings**
|
||||
4) Search for `telemetry`
|
||||
5) Uncheck the **Telemetry: Enable Telemetry** setting
|
|
@ -27,5 +27,7 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio
|
|||
|
||||
## Documentation
|
||||
|
||||
- [Installation instructions](/docs/install.md)
|
||||
- [How to use the Extension](/docs/how-to-use.md)
|
||||
- [Setup for developers](/docs/developers-setup.md)
|
||||
- [Contributing](CONTRIBUTING.md)
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
- Download link : https://nodejs.org/en/download/
|
||||
|
||||
- Python 3 (or latest)
|
||||
- Python 3.7.4 (or latest)
|
||||
|
||||
- Download link : https://www.python.org/downloads/
|
||||
- /!\ Make sure Python is in your path (during installation or insert it manually afterwards)
|
||||
- /!\ Make sure pip is added to your environment variables as well
|
||||
- **NOTE :** Make sure Python is in your path under an environment variable named `python` (during installation or insert it manually afterwards)
|
||||
- **NOTE :** Make sure pip is added to your environment variables as well
|
||||
(for example it could be find at : c:\users\<alias>\appdata\local\programs\python\python37\lib\site-packages\pip)
|
||||
- Run in a console `python -m pip install --upgrade pip`
|
||||
|
||||
|
@ -21,6 +21,10 @@
|
|||
(Link to download : https://visualstudio.microsoft.com/vs/older-downloads under
|
||||
'Redistributables and Build tools' : 'Microsoft Build Tools 2015')
|
||||
|
||||
- Pywin32
|
||||
|
||||
- Run the command in a console : `pip install pywin32`
|
||||
|
||||
- VS Code
|
||||
|
||||
- Python extension for VS Code (download from VS Code market place)
|
||||
|
@ -41,10 +45,11 @@
|
|||
|
||||
## Notes on how to use it
|
||||
|
||||
- [Documentation to use the Extension](/docs/how-to-use.md)
|
||||
- Debugging the extension opens a new VS Code window with the extension installed
|
||||
- From the original VS Code window (opened in our repository) you can see outputs in the Debug Console
|
||||
- In the new VS Code window, you can access the commands provided by the extension from the Commands Palette (Ctrl+Shift+P)
|
||||
listed as 'Adafruit : ...'
|
||||
listed as 'Pacifica : ...'
|
||||
- If you change some files you'll need to run the 'npm run compile' command again and restart debugging
|
||||
|
||||
## Repository Structure (important files)
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
# How to use the Extension
|
||||
|
||||
Commands are accessible through :
|
||||
|
||||
- **The command palette** (`Ctrl+shift+P` or `View->Command Palette`) and type 'Pacifica : `command_name`'
|
||||
- **The extension buttons** available on the top right of the Text Editor Panel when you have a Python file open
|
||||
|
||||
## Available commands
|
||||
|
||||
- **Open Simulator** : opens the webview of the simulator.
|
||||
|
||||
- **New Project** : opens an unsaved file with links to help you and a code snippet that you can save as `code.py` / `main.py`.
|
||||
_(**Note :** will open the simulator webview if it's not open yet)_.
|
||||
|
||||
- **Run Simulator** : run the code you have open on the simulator (make sure you've clicked on a valid code file).
|
||||
_(**Note :** will open the simulator webview if it's not open yet)_.
|
||||
|
||||
- **Deploy to Device** : saves the code to a Circuit Playground Express.
|
||||
_(**Note :** the board needs to be correctly formatted to a `CIRCUITPY` drive first if it's not the case : [Installing CircuitPython](https://learn.adafruit.com/welcome-to-circuitpython/installing-circuitpython))_.
|
||||
|
||||
## Available features
|
||||
|
||||
- We currently support the [Adafruit Circuit Playground Express board](https://www.adafruit.com/product/3333)
|
||||
- Access to auto-completion and Python error flagging
|
||||
- Output panel for the simulator (without print statements)
|
||||
- Deploy to the physical device (if correctly formatted)
|
||||
- Device's features :
|
||||
- NeoPixels
|
||||
- Buttons (A & B)
|
||||
- Sound - .wav files
|
||||
- Red LED
|
||||
- Switch
|
||||
|
||||
## Not supported yet
|
||||
|
||||
- User print statements
|
||||
- Updating the simulator's state without needing to call the`show` method
|
||||
- Auto-detect/format the device
|
||||
- Serial monitor for the device
|
||||
- Debugger for the simulator
|
||||
- Device's features
|
||||
- Light sensor
|
||||
- Temperature sensor
|
||||
- Motion sensors
|
||||
- Sound sensor
|
||||
- Touch sensors
|
||||
- Sound - tones
|
||||
- Green LED
|
||||
- IR transmitter
|
||||
|
||||
## Troubleshooting Tips
|
||||
|
||||
- The first time you install the extension, you'll need to execute the `run` command at least once in order to access auto-completion.
|
||||
- While running a code file, if you get an error saying it can't find the file, make sure you've clicked on a valid Python code file before running it.
|
||||
- To open the output panel again after closing it go to VS Code menu : `View->Output`.
|
||||
- If you have pylint enabled, it might underline the import of the adafruit_circuitplayground library, but it will work correctly.
|
||||
- If you try to deploy to the device while it's plugged in but you still get an error saying it cannot find the board, make sure your Circuit Playground Express is formatted correctly and that its name matches `CIRCUITPY`.
|
|
@ -0,0 +1,28 @@
|
|||
# Instructions on How to Install and Run the Extension
|
||||
|
||||
## Steps to manually install the extension
|
||||
|
||||
1. Link to the latest releases :
|
||||
[Releases](https://github.com/microsoft/vscode-python-embedded/releases)
|
||||
2. Click on the latest release
|
||||
3. At the bottom of the page download the .vsix file
|
||||
4. To install the .vsix file :
|
||||
- Go to the directory where the downloaded vsix file is and run in a command console: `code --install-extension <vsix file name>`
|
||||
- Or in VS Code, go to the extension tab (a), in menu (b) select 'Install from VSIX' (c) and search the file you downloaded
|
||||
![VSIX Install Instructions](./vsix-install-instructions.png)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/Download)
|
||||
- [Node](https://nodejs.org/en/download/)
|
||||
- [Python 3.7.4 (or latest)](https://www.python.org/downloads/)
|
||||
- Python VS Code extension (download from VS Code Marketplace)
|
||||
- Simple audio :
|
||||
- `python -m pip install --upgrade pip`
|
||||
- `pip install simpleaudio`
|
||||
- **Troubleshoot :** If it's not working make sure you have pip and C++ 2015 build tools installed ([Download link](https://visualstudio.microsoft.com/vs/older-downloads), and look under 'Redistributables and Build tools' : 'Microsoft Build Tools 2015')
|
||||
- Pywin32 : `pip install pywin32`
|
||||
|
||||
## How to use the extension
|
||||
|
||||
- [How to use the Extension](/docs/how-to-use.md)
|
|
@ -0,0 +1,25 @@
|
|||
# Pacifica Telemetry
|
||||
|
||||
Pacifica logs usage data and diagnostics telemetry through [Application Insights](https://azure.microsoft.com/en-us/services/monitor/).
|
||||
|
||||
## Telemetry Gathered
|
||||
|
||||
This extension collects basic diagnostics telemetry and usage data:
|
||||
|
||||
- **Diagnostics telemetry**: performance of extension commands and success / error rate
|
||||
- **Usage telemetry**: user usage of extension commands and API calls
|
||||
|
||||
## Usage Telemetry
|
||||
|
||||
Through the Application Insights API, telemetry events are collected on Pacifica extension usage. The follow table describes the Telemetry events we collect:
|
||||
|
||||
| **Property** | **Note** |
|
||||
| :-------------------: | ---------------------------------------------------------------------------------------------------- |
|
||||
| **Event Name** | Unique event name/descriptor for the event. For ex: Pacifica/COMMAND_NEW_PROJECT |
|
||||
| **VS Code Session ID** | A unique identifier for the current session (changes each time the editor is started) |
|
||||
| **VS Code Machine ID** | A unique identifier for the computer |
|
||||
| **VS Code Version** | VS Code version being used by the user |
|
||||
| **Extension Version** | Pacifica extension version being used |
|
||||
| **OS** | User's operating system |
|
||||
| **Performance** | A number indicating how long the command or API call took to execute |
|
||||
| **Result** | If the event succeeded or not |
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 67 KiB |
|
@ -4,15 +4,17 @@
|
|||
"dialogResponses.help": "I need help",
|
||||
"dialogResponses.tutorials": "Tutorials on Adafruit",
|
||||
"error.noDevice": "No plugged in boards detected. Please double check if your board is connected and/or properly formatted",
|
||||
"error.stderr": "[ERROR] {0} \n",
|
||||
"error.noFileToRun": "\n[ERROR] We can't find the .py file to run on simulator. Open up a new .py file, or browse through some examples\n",
|
||||
"error.stderr": "\n[ERROR] {0} \n",
|
||||
"error.unexpectedMessage": "Webview sent an unexpected message",
|
||||
"info.deployDevice": "\n[INFO] Deploying code to the device...\n",
|
||||
"info.deploySimulator": "\n[INFO] Deploying code to the simulator...\n",
|
||||
"info.deploySuccess": "\n[INFO] Code successfully deployed\n",
|
||||
"info.extensionActivated": "Congratulations, your extension Adafruit_Simulator is now active!",
|
||||
"info.firstTimeWebview": "To reopen the simulator click on the \"Open Simulator\" button on the upper right corner of the text editor, or select the command \"Open Simulator\" from command palette.",
|
||||
"info.newProject": "New to Python or Circuit Playground Express project? We are here to help!",
|
||||
"info.runningCode": "Running user code",
|
||||
"info.welcomeOutputTab": "Welcome to the Adafruit Simulator output tab !\n\n",
|
||||
"label.webviewPanel": "Adafruit CPX",
|
||||
"name": "Adafruit Simulator"
|
||||
}
|
||||
}
|
||||
|
|
81
package.json
81
package.json
|
@ -27,7 +27,8 @@
|
|||
"onCommand:pacifica.openSimulator",
|
||||
"onCommand:pacifica.runSimulator",
|
||||
"onCommand:pacifica.newProject",
|
||||
"onCommand:pacifica.runDevice"
|
||||
"onCommand:pacifica.runDevice",
|
||||
"onDebug"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
|
@ -98,7 +99,81 @@
|
|||
"scope": "resource"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"breakpoints": [
|
||||
{
|
||||
"language": "python"
|
||||
}
|
||||
],
|
||||
"debuggers": [
|
||||
{
|
||||
"type": "python",
|
||||
"label": "Pacifica Simulator Debugger",
|
||||
"program": "./out/debugAdapter.js",
|
||||
"runtime": "node",
|
||||
"configurationAttributes": {
|
||||
"launch": {
|
||||
"properties": {
|
||||
"program": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the code file.",
|
||||
"default": "${file}"
|
||||
},
|
||||
"stopOnEntry": {
|
||||
"type": "boolean",
|
||||
"description": "Automatically stop after launch.",
|
||||
"default": false
|
||||
},
|
||||
"justMyCode": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "Command line arguments passed to the program.",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"description": "Debugger rules.",
|
||||
"default": [],
|
||||
"items": {
|
||||
"path": "string",
|
||||
"include": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"name": "Pacifica Simulator Debugger",
|
||||
"program": "${file}",
|
||||
"stopOnEntry": false,
|
||||
"justMyCode": true
|
||||
}
|
||||
],
|
||||
"configurationSnippets": [
|
||||
{
|
||||
"label": "Pacifica Simulator Debugger : Launch",
|
||||
"description": "Pacifica Simulator Debugger - A configuration for debugging a python code file for the Pacifica simulator.",
|
||||
"body": {
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"name": "Pacifica Simulator Debugger",
|
||||
"program": "${file}",
|
||||
"stopOnEntry": false,
|
||||
"justMyCode": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
|
@ -165,4 +240,4 @@
|
|||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,13 +9,26 @@ const localize: nls.LocalizeFunc = nls.config({
|
|||
})();
|
||||
|
||||
export const CONSTANTS = {
|
||||
DEBUG_CONFIGURATION_NAME: "Pacifica Simulator Debugger",
|
||||
ERROR: {
|
||||
INVALID_FILE_NAME_DEBUG: localize(
|
||||
"error.invalidFileNameDebug",
|
||||
'The file you tried to run isn\'t named "code.py" or "main.py". Rename your file if you wish to debug it.'
|
||||
),
|
||||
NO_DEVICE: localize(
|
||||
"error.noDevice",
|
||||
"No plugged in boards detected. Please double check if your board is connected and/or properly formatted"
|
||||
),
|
||||
NO_FILE_TO_RUN: localize(
|
||||
"error.noFileToRun",
|
||||
"\n[ERROR] We can't find the .py file to run. Open up a new .py file, or browse through some examples to start with: https://github.com/adafruit/Adafruit_CircuitPython_CircuitPlayground/tree/master/examples\n"
|
||||
),
|
||||
NO_PROGRAM_FOUND_DEBUG: localize(
|
||||
"error.noProgramFoundDebug",
|
||||
"Cannot find a program to debug."
|
||||
),
|
||||
STDERR: (data: string) => {
|
||||
return localize("error.stderr", `[ERROR] ${data} \n`);
|
||||
return localize("error.stderr", `\n[ERROR] ${data} \n`);
|
||||
},
|
||||
UNEXPECTED_MESSAGE: localize(
|
||||
"error.unexpectedMessage",
|
||||
|
@ -40,6 +53,10 @@ export const CONSTANTS = {
|
|||
"info.extensionActivated",
|
||||
"Congratulations, your extension Adafruit_Simulator is now active!"
|
||||
),
|
||||
FIRST_TIME_WEBVIEW: localize(
|
||||
"info.firstTimeWebview",
|
||||
'To reopen the simulator click on the "Open Simulator" button on the upper right corner of the text editor, or select the command "Open Simulator" from command palette.'
|
||||
),
|
||||
NEW_PROJECT: localize(
|
||||
"info.newProject",
|
||||
"New to Python or Circuit Playground Express project? We are here to help!"
|
||||
|
@ -91,8 +108,18 @@ export enum TelemetryEventName {
|
|||
ERROR_COMMAND_NEW_PROJECT = "ERROR.COMMAND.NEW.PROJECT",
|
||||
ERROR_DEPLOY_WITHOUT_DEVICE = "ERROR.DEPLOY.WITHOUT.DEVICE",
|
||||
|
||||
SUCCESS_COMMAND_DEPLOY_DEVICE = "SUCCESS.COMMAND.DEPLOY.DEVICE"
|
||||
}
|
||||
SUCCESS_COMMAND_DEPLOY_DEVICE = "SUCCESS.COMMAND.DEPLOY.DEVICE",
|
||||
|
||||
// Performance
|
||||
PERFORMANCE_DEPLOY_DEVICE = "PERFORMANCE.DEPLOY.DEVICE",
|
||||
PERFORMANCE_NEW_PROJECT = "PERFORMANCE.NEW.PROJECT",
|
||||
PERFORMANCE_OPEN_SIMULATOR = "PERFORMANCE.OPEN.SIMULATOR"
|
||||
}
|
||||
|
||||
export enum WebviewMessages {
|
||||
BUTTON_PRESS = "button-press",
|
||||
PLAY_SIMULATOR = "play-simulator"
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: no-namespace
|
||||
export namespace DialogResponses {
|
||||
|
@ -110,4 +137,9 @@ export namespace DialogResponses {
|
|||
};
|
||||
}
|
||||
|
||||
export default CONSTANTS;
|
||||
export const USER_CODE_NAMES = {
|
||||
CODE_PY: "code.py",
|
||||
MAIN_PY: "main.py"
|
||||
};
|
||||
|
||||
export default CONSTANTS;
|
||||
|
|
465
src/extension.ts
465
src/extension.ts
|
@ -7,11 +7,20 @@ import * as cp from "child_process";
|
|||
import * as fs from "fs";
|
||||
import * as open from "open";
|
||||
import TelemetryAI from "./telemetry/telemetryAI";
|
||||
import { CONSTANTS, DialogResponses, TelemetryEventName } from "./constants";
|
||||
import {
|
||||
CONSTANTS,
|
||||
DialogResponses,
|
||||
TelemetryEventName,
|
||||
WebviewMessages
|
||||
} from "./constants";
|
||||
import { SimulatorDebugConfigurationProvider } from "./simulatorDebugConfigurationProvider";
|
||||
import * as utils from "./utils";
|
||||
|
||||
let currentFileAbsPath: string = "";
|
||||
// Notification booleans
|
||||
let firstTimeClosed: boolean = true;
|
||||
let shouldShowNewProject: boolean = true;
|
||||
|
||||
|
||||
function loadScript(context: vscode.ExtensionContext, path: string) {
|
||||
return `<script src="${vscode.Uri.file(context.asAbsolutePath(path))
|
||||
.with({ scheme: "vscode-resource" })
|
||||
|
@ -30,7 +39,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
|
||||
// Add our library path to settings.json for autocomplete functionality
|
||||
updatePythonExtraPaths();
|
||||
|
||||
|
||||
if (outChannel === undefined) {
|
||||
outChannel = vscode.window.createOutputChannel(CONSTANTS.NAME);
|
||||
logToOutputChannel(outChannel, CONSTANTS.INFO.WELCOME_OUTPUT_TAB, true);
|
||||
|
@ -43,7 +52,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
currentPanel = vscode.window.createWebviewPanel(
|
||||
"adafruitSimulator",
|
||||
CONSTANTS.LABEL.WEBVIEW_PANEL,
|
||||
vscode.ViewColumn.Two,
|
||||
{ preserveFocus: true, viewColumn: vscode.ViewColumn.Two },
|
||||
{
|
||||
// Only allow the webview to access resources in our extension's media directory
|
||||
localResourceRoots: [
|
||||
|
@ -52,12 +61,59 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
enableScripts: true
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
currentPanel.webview.html = getWebviewContent(context);
|
||||
|
||||
if (messageListener !== undefined) {
|
||||
messageListener.dispose();
|
||||
const index = context.subscriptions.indexOf(messageListener);
|
||||
if (index > -1) {
|
||||
context.subscriptions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPanel) {
|
||||
// Handle messages from webview
|
||||
messageListener = currentPanel.webview.onDidReceiveMessage(
|
||||
message => {
|
||||
switch (message.command) {
|
||||
case WebviewMessages.BUTTON_PRESS:
|
||||
// Send input to the Python process
|
||||
handleButtonPressTelemetry(message.text);
|
||||
console.log("About to write");
|
||||
console.log(JSON.stringify(message.text) + "\n");
|
||||
childProcess.stdin.write(JSON.stringify(message.text) + "\n");
|
||||
break;
|
||||
case WebviewMessages.PLAY_SIMULATOR:
|
||||
console.log("Play button");
|
||||
console.log(JSON.stringify(message.text) + "\n");
|
||||
if (message.text as boolean) {
|
||||
runSimulatorCommand();
|
||||
} else {
|
||||
killProcessIfRunning();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
vscode.window.showInformationMessage(
|
||||
CONSTANTS.ERROR.UNEXPECTED_MESSAGE
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
}
|
||||
|
||||
currentPanel.onDidDispose(
|
||||
() => {
|
||||
currentPanel = undefined;
|
||||
if (firstTimeClosed) {
|
||||
vscode.window.showInformationMessage(
|
||||
CONSTANTS.INFO.FIRST_TIME_WEBVIEW
|
||||
);
|
||||
firstTimeClosed = false;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
|
@ -70,215 +126,195 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
"pacifica.openSimulator",
|
||||
() => {
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.COMMAND_OPEN_SIMULATOR);
|
||||
openWebview();
|
||||
TelemetryAI.runWithLatencyMeasure(
|
||||
openWebview,
|
||||
TelemetryEventName.PERFORMANCE_OPEN_SIMULATOR
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const openTemplateFile = () => {
|
||||
const fileName = "template.py";
|
||||
const filePath = __dirname + path.sep + fileName;
|
||||
const file = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
if (shouldShowNewProject) {
|
||||
vscode.window
|
||||
.showInformationMessage(
|
||||
CONSTANTS.INFO.NEW_PROJECT,
|
||||
...[
|
||||
DialogResponses.DONT_SHOW,
|
||||
DialogResponses.EXAMPLE_CODE,
|
||||
DialogResponses.TUTORIALS
|
||||
]
|
||||
)
|
||||
.then((selection: vscode.MessageItem | undefined) => {
|
||||
if (selection === DialogResponses.DONT_SHOW) {
|
||||
shouldShowNewProject = false;
|
||||
TelemetryAI.trackFeatureUsage(
|
||||
TelemetryEventName.CLICK_DIALOG_DONT_SHOW
|
||||
);
|
||||
} else if (selection === DialogResponses.EXAMPLE_CODE) {
|
||||
open(CONSTANTS.LINKS.EXAMPLE_CODE);
|
||||
TelemetryAI.trackFeatureUsage(
|
||||
TelemetryEventName.CLICK_DIALOG_EXAMPLE_CODE
|
||||
);
|
||||
} else if (selection === DialogResponses.TUTORIALS) {
|
||||
open(CONSTANTS.LINKS.TUTORIALS);
|
||||
TelemetryAI.trackFeatureUsage(
|
||||
TelemetryEventName.CLICK_DIALOG_TUTORIALS
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openWebview();
|
||||
|
||||
vscode.workspace
|
||||
.openTextDocument({ content: file, language: "python" })
|
||||
.then((template: vscode.TextDocument) => {
|
||||
vscode.window.showTextDocument(template, 1, false);
|
||||
}),
|
||||
(error: any) => {
|
||||
TelemetryAI.trackFeatureUsage(
|
||||
TelemetryEventName.ERROR_COMMAND_NEW_PROJECT
|
||||
);
|
||||
console.error(`Failed to open a new text document: ${error}`);
|
||||
};
|
||||
};
|
||||
|
||||
const newProject: vscode.Disposable = vscode.commands.registerCommand(
|
||||
"pacifica.newProject",
|
||||
() => {
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.COMMAND_NEW_PROJECT);
|
||||
|
||||
const fileName = "template.py";
|
||||
const filePath = __dirname + path.sep + fileName;
|
||||
const file = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
|
||||
if (shouldShowNewProject) {
|
||||
vscode.window
|
||||
.showInformationMessage(
|
||||
CONSTANTS.INFO.NEW_PROJECT,
|
||||
...[
|
||||
DialogResponses.DONT_SHOW,
|
||||
DialogResponses.EXAMPLE_CODE,
|
||||
DialogResponses.TUTORIALS
|
||||
]
|
||||
)
|
||||
.then((selection: vscode.MessageItem | undefined) => {
|
||||
if (selection === DialogResponses.DONT_SHOW) {
|
||||
shouldShowNewProject = false;
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.CLICK_DIALOG_DONT_SHOW);
|
||||
} else if (selection === DialogResponses.EXAMPLE_CODE) {
|
||||
open(CONSTANTS.LINKS.EXAMPLE_CODE);
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.CLICK_DIALOG_EXAMPLE_CODE);
|
||||
} else if (selection === DialogResponses.TUTORIALS) {
|
||||
open(CONSTANTS.LINKS.TUTORIALS);
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.CLICK_DIALOG_TUTORIALS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openWebview();
|
||||
|
||||
|
||||
vscode.workspace
|
||||
.openTextDocument({ content: file, language: "python" })
|
||||
.then((template: vscode.TextDocument) => {
|
||||
vscode.window.showTextDocument(template, 1, false);
|
||||
}),
|
||||
(error: any) => {
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.ERROR_COMMAND_NEW_PROJECT);
|
||||
console.error(`Failed to open a new text document: ${error}`);
|
||||
};
|
||||
TelemetryAI.runWithLatencyMeasure(
|
||||
openTemplateFile,
|
||||
TelemetryEventName.PERFORMANCE_NEW_PROJECT
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const killProcessIfRunning = () => {
|
||||
if (childProcess !== undefined) {
|
||||
if (currentPanel) {
|
||||
console.info("Sending clearing state command");
|
||||
currentPanel.webview.postMessage({ command: "reset-state" });
|
||||
}
|
||||
// TODO: We need to check the process was correctly killed
|
||||
childProcess.kill();
|
||||
}
|
||||
};
|
||||
|
||||
const runSimulatorCommand = () => {
|
||||
openWebview();
|
||||
|
||||
if (!currentPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(CONSTANTS.INFO.RUNNING_CODE);
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.COMMAND_RUN_SIMULATOR);
|
||||
|
||||
logToOutputChannel(outChannel, CONSTANTS.INFO.DEPLOY_SIMULATOR);
|
||||
|
||||
const activeTextEditor: vscode.TextEditor | undefined =
|
||||
vscode.window.activeTextEditor;
|
||||
|
||||
updateCurrentFileIfPython(activeTextEditor);
|
||||
|
||||
if (currentFileAbsPath === "") {
|
||||
logToOutputChannel(outChannel, CONSTANTS.ERROR.NO_FILE_TO_RUN, true);
|
||||
}
|
||||
|
||||
killProcessIfRunning();
|
||||
|
||||
childProcess = cp.spawn("python", [
|
||||
utils.getPathToScript(context, "out", "process_user_code.py"),
|
||||
currentFileAbsPath
|
||||
]);
|
||||
|
||||
let dataFromTheProcess = "";
|
||||
let oldMessage = "";
|
||||
|
||||
// Data received from Python process
|
||||
childProcess.stdout.on("data", data => {
|
||||
dataFromTheProcess = data.toString();
|
||||
if (currentPanel) {
|
||||
// Process the data from the process and send one state at a time
|
||||
dataFromTheProcess.split("\0").forEach(message => {
|
||||
if (currentPanel && message.length > 0 && message != oldMessage) {
|
||||
oldMessage = message;
|
||||
let messageToWebview;
|
||||
// Check the message is a JSON
|
||||
try {
|
||||
messageToWebview = JSON.parse(message);
|
||||
// Check the JSON is a state
|
||||
switch (messageToWebview.type) {
|
||||
case "state":
|
||||
console.log(
|
||||
`Process state output = ${messageToWebview.data}`
|
||||
);
|
||||
currentPanel.webview.postMessage({
|
||||
command: "set-state",
|
||||
state: JSON.parse(messageToWebview.data)
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(
|
||||
`Non-state JSON output from the process : ${messageToWebview}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Non-JSON output from the process : ${message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Std error output
|
||||
childProcess.stderr.on("data", data => {
|
||||
console.error(`Error from the Python process through stderr: ${data}`);
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.ERROR_PYTHON_PROCESS);
|
||||
logToOutputChannel(outChannel, CONSTANTS.ERROR.STDERR(data), true);
|
||||
if (currentPanel) {
|
||||
console.log("Sending clearing state command");
|
||||
currentPanel.webview.postMessage({ command: "reset-state" });
|
||||
}
|
||||
});
|
||||
|
||||
// When the process is done
|
||||
childProcess.on("end", (code: number) => {
|
||||
console.info(`Command execution exited with code: ${code}`);
|
||||
});
|
||||
};
|
||||
|
||||
// Send message to the webview
|
||||
const runSimulator: vscode.Disposable = vscode.commands.registerCommand(
|
||||
"pacifica.runSimulator",
|
||||
() => {
|
||||
openWebview();
|
||||
|
||||
if (!currentPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.COMMAND_RUN_SIMULATOR);
|
||||
|
||||
console.info(CONSTANTS.INFO.RUNNING_CODE);
|
||||
const activeTextEditor: vscode.TextEditor | undefined =
|
||||
vscode.window.activeTextEditor;
|
||||
let currentFileAbsPath: string = "";
|
||||
|
||||
if (activeTextEditor) {
|
||||
currentFileAbsPath = activeTextEditor.document.fileName;
|
||||
}
|
||||
|
||||
// Get the Python script path (And the special URI to use with the webview)
|
||||
const onDiskPath = vscode.Uri.file(
|
||||
path.join(context.extensionPath, "out", "process_user_code.py")
|
||||
);
|
||||
const scriptPath = onDiskPath.with({ scheme: "vscode-resource" });
|
||||
|
||||
// Create the Python process (after killing the one running if any)
|
||||
if (childProcess !== undefined) {
|
||||
if (currentPanel) {
|
||||
console.info("Sending clearing state command");
|
||||
currentPanel.webview.postMessage({ command: "reset-state" });
|
||||
}
|
||||
// TODO: We need to check the process was correctly killed
|
||||
childProcess.kill();
|
||||
}
|
||||
|
||||
logToOutputChannel(outChannel, CONSTANTS.INFO.DEPLOY_SIMULATOR);
|
||||
|
||||
childProcess = cp.spawn("python", [
|
||||
scriptPath.fsPath,
|
||||
currentFileAbsPath
|
||||
]);
|
||||
|
||||
let dataFromTheProcess = "";
|
||||
let oldMessage = "";
|
||||
|
||||
// Data received from Python process
|
||||
childProcess.stdout.on("data", data => {
|
||||
dataFromTheProcess = data.toString();
|
||||
if (currentPanel) {
|
||||
// Process the data from the process and send one state at a time
|
||||
dataFromTheProcess.split("\0").forEach(message => {
|
||||
if (currentPanel && message.length > 0 && message != oldMessage) {
|
||||
oldMessage = message;
|
||||
let messageToWebview;
|
||||
// Check the message is a JSON
|
||||
try {
|
||||
messageToWebview = JSON.parse(message);
|
||||
// Check the JSON is a state
|
||||
switch (messageToWebview.type) {
|
||||
case "state":
|
||||
console.log(
|
||||
`Process state output = ${messageToWebview.data}`
|
||||
);
|
||||
currentPanel.webview.postMessage({
|
||||
command: "set-state",
|
||||
state: JSON.parse(messageToWebview.data)
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(
|
||||
`Non-state JSON output from the process : ${messageToWebview}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Non-JSON output from the process : ${message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Std error output
|
||||
childProcess.stderr.on("data", data => {
|
||||
console.error(`Error from the Python process through stderr: ${data}`);
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.ERROR_PYTHON_PROCESS);
|
||||
logToOutputChannel(outChannel, CONSTANTS.ERROR.STDERR(data), true);
|
||||
if (currentPanel) {
|
||||
console.log("Sending clearing state command");
|
||||
currentPanel.webview.postMessage({ command: "reset-state" });
|
||||
}
|
||||
});
|
||||
|
||||
// When the process is done
|
||||
childProcess.on("end", (code: number) => {
|
||||
console.info(`Command execution exited with code: ${code}`);
|
||||
});
|
||||
|
||||
if (messageListener !== undefined) {
|
||||
messageListener.dispose();
|
||||
const index = context.subscriptions.indexOf(messageListener);
|
||||
if (index > -1) {
|
||||
context.subscriptions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle messages from webview
|
||||
messageListener = currentPanel.webview.onDidReceiveMessage(
|
||||
message => {
|
||||
switch (message.command) {
|
||||
case "button-press":
|
||||
// Send input to the Python process
|
||||
handleButtonPressTelemetry(message.text);
|
||||
console.log("About to write");
|
||||
console.log(JSON.stringify(message.text) + "\n");
|
||||
childProcess.stdin.write(JSON.stringify(message.text) + "\n");
|
||||
break;
|
||||
default:
|
||||
vscode.window.showInformationMessage(
|
||||
CONSTANTS.ERROR.UNEXPECTED_MESSAGE
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
runSimulatorCommand();
|
||||
}
|
||||
);
|
||||
|
||||
// Send message to the webview
|
||||
const runDevice: vscode.Disposable = vscode.commands.registerCommand("pacifica.runDevice", () => {
|
||||
const deployCodeToDevice = () => {
|
||||
console.info("Sending code to device");
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.COMMAND_DEPLOY_DEVICE);
|
||||
|
||||
logToOutputChannel(outChannel, CONSTANTS.INFO.DEPLOY_DEVICE);
|
||||
|
||||
const activeTextEditor: vscode.TextEditor | undefined =
|
||||
vscode.window.activeTextEditor;
|
||||
let currentFileAbsPath: string = "";
|
||||
|
||||
if (activeTextEditor) {
|
||||
currentFileAbsPath = activeTextEditor.document.fileName;
|
||||
updateCurrentFileIfPython(activeTextEditor);
|
||||
|
||||
if (currentFileAbsPath === "") {
|
||||
logToOutputChannel(outChannel, CONSTANTS.ERROR.NO_FILE_TO_RUN, true);
|
||||
}
|
||||
|
||||
// Get the Python script path (And the special URI to use with the webview)
|
||||
const onDiskPath = vscode.Uri.file(
|
||||
path.join(context.extensionPath, "out", "device.py")
|
||||
);
|
||||
const scriptPath = onDiskPath.with({ scheme: "vscode-resource" });
|
||||
|
||||
const deviceProcess = cp.spawn("python", [
|
||||
scriptPath.fsPath,
|
||||
utils.getPathToScript(context, "out", "device.py"),
|
||||
currentFileAbsPath
|
||||
]);
|
||||
|
||||
|
@ -294,12 +330,16 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
// Check the JSON is a state
|
||||
switch (messageToWebview.type) {
|
||||
case "complete":
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.SUCCESS_COMMAND_DEPLOY_DEVICE);
|
||||
TelemetryAI.trackFeatureUsage(
|
||||
TelemetryEventName.SUCCESS_COMMAND_DEPLOY_DEVICE
|
||||
);
|
||||
logToOutputChannel(outChannel, CONSTANTS.INFO.DEPLOY_SUCCESS);
|
||||
break;
|
||||
|
||||
case "no-device":
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.ERROR_DEPLOY_WITHOUT_DEVICE);
|
||||
TelemetryAI.trackFeatureUsage(
|
||||
TelemetryEventName.ERROR_DEPLOY_WITHOUT_DEVICE
|
||||
);
|
||||
vscode.window
|
||||
.showErrorMessage(
|
||||
CONSTANTS.ERROR.NO_DEVICE,
|
||||
|
@ -307,7 +347,9 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
)
|
||||
.then((selection: vscode.MessageItem | undefined) => {
|
||||
if (selection === DialogResponses.HELP) {
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.CLICK_DIALOG_HELP_DEPLOY_TO_DEVICE);
|
||||
TelemetryAI.trackFeatureUsage(
|
||||
TelemetryEventName.CLICK_DIALOG_HELP_DEPLOY_TO_DEVICE
|
||||
);
|
||||
open(CONSTANTS.LINKS.HELP);
|
||||
}
|
||||
});
|
||||
|
@ -328,7 +370,10 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
|
||||
// Std error output
|
||||
deviceProcess.stderr.on("data", data => {
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.ERROR_PYTHON_DEVICE_PROCESS, { error: `${data}` });
|
||||
TelemetryAI.trackFeatureUsage(
|
||||
TelemetryEventName.ERROR_PYTHON_DEVICE_PROCESS,
|
||||
{ error: `${data}` }
|
||||
);
|
||||
console.error(
|
||||
`Error from the Python device process through stderr: ${data}`
|
||||
);
|
||||
|
@ -339,16 +384,44 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
deviceProcess.on("end", (code: number) => {
|
||||
console.info(`Command execution exited with code: ${code}`);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const runDevice: vscode.Disposable = vscode.commands.registerCommand(
|
||||
"pacifica.runDevice",
|
||||
() => {
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.COMMAND_DEPLOY_DEVICE);
|
||||
TelemetryAI.runWithLatencyMeasure(
|
||||
deployCodeToDevice,
|
||||
TelemetryEventName.PERFORMANCE_DEPLOY_DEVICE
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Debugger configuration
|
||||
const simulatorDebugConfiguration = new SimulatorDebugConfigurationProvider(
|
||||
utils.getPathToScript(context, "out", "process_user_code.py")
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
openSimulator,
|
||||
runSimulator,
|
||||
runDevice,
|
||||
newProject
|
||||
newProject,
|
||||
vscode.debug.registerDebugConfigurationProvider(
|
||||
"python",
|
||||
simulatorDebugConfiguration
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const updateCurrentFileIfPython = (
|
||||
activeTextEditor: vscode.TextEditor | undefined
|
||||
) => {
|
||||
if (activeTextEditor && activeTextEditor.document.languageId === "python") {
|
||||
currentFileAbsPath = activeTextEditor.document.fileName;
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonPressTelemetry = (buttonState: any) => {
|
||||
if (buttonState["button_a"] && buttonState["button_b"]) {
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.SIMULATOR_BUTTON_AB);
|
||||
|
@ -359,7 +432,7 @@ const handleButtonPressTelemetry = (buttonState: any) => {
|
|||
} else if (buttonState["switch"]) {
|
||||
TelemetryAI.trackFeatureUsage(TelemetryEventName.SIMULATOR_SWITCH);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updatePythonExtraPaths = () => {
|
||||
const pathToLib: string = __dirname;
|
||||
|
@ -384,7 +457,7 @@ const logToOutputChannel = (
|
|||
show: boolean = false
|
||||
) => {
|
||||
if (outChannel) {
|
||||
if (show) outChannel.show();
|
||||
if (show) outChannel.show(true);
|
||||
outChannel.append(message);
|
||||
}
|
||||
};
|
||||
|
@ -410,4 +483,4 @@ function getWebviewContent(context: vscode.ExtensionContext) {
|
|||
}
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
export function deactivate() { }
|
||||
export function deactivate() {}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import * as vscode from "vscode";
|
||||
import { validCodeFileName } from "./utils";
|
||||
import { CONSTANTS } from "./constants";
|
||||
|
||||
export class SimulatorDebugConfigurationProvider
|
||||
implements vscode.DebugConfigurationProvider {
|
||||
constructor(private pathToScript: string) {}
|
||||
|
||||
/**
|
||||
* Modify the debug configuration just before a debug session is being launched.
|
||||
*/
|
||||
public resolveDebugConfiguration(
|
||||
folder: vscode.WorkspaceFolder | undefined,
|
||||
config: vscode.DebugConfiguration,
|
||||
token?: vscode.CancellationToken
|
||||
): vscode.ProviderResult<vscode.DebugConfiguration> {
|
||||
// Check config name
|
||||
if (config.name === CONSTANTS.DEBUG_CONFIGURATION_NAME) {
|
||||
const activeTextEditor = vscode.window.activeTextEditor;
|
||||
if (activeTextEditor) {
|
||||
const currentFilePath = activeTextEditor.document.fileName;
|
||||
|
||||
// Check file type and name
|
||||
if (
|
||||
!(activeTextEditor.document.languageId === "python") ||
|
||||
!validCodeFileName(currentFilePath)
|
||||
) {
|
||||
return vscode.window
|
||||
.showErrorMessage(CONSTANTS.ERROR.INVALID_FILE_NAME_DEBUG)
|
||||
.then(() => {
|
||||
return undefined; // Abort launch
|
||||
});
|
||||
}
|
||||
// Set process_user_code path as program
|
||||
config.program = this.pathToScript;
|
||||
// Set user's code path as args
|
||||
config.args = [currentFilePath];
|
||||
// Set rules
|
||||
config.rules = [
|
||||
{ path: this.pathToScript, include: false },
|
||||
{
|
||||
module: "adafruit_circuitplayground",
|
||||
include: false
|
||||
},
|
||||
{ module: "simpleaudio", include: false }
|
||||
];
|
||||
}
|
||||
}
|
||||
// Abort / show error message if can't find process_user_code.py
|
||||
if (!config.program) {
|
||||
return vscode.window
|
||||
.showInformationMessage(CONSTANTS.ERROR.NO_PROGRAM_FOUND_DEBUG)
|
||||
.then(() => {
|
||||
return undefined; // Abort launch
|
||||
});
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
|
@ -4,10 +4,21 @@ import getPackageInfo from "./getPackageInfo";
|
|||
|
||||
// tslint:disable-next-line:export-name
|
||||
export default class TelemetryAI {
|
||||
static trackFeatureUsage(eventName: string, eventProperties?: { [key: string]: string }) {
|
||||
public static trackFeatureUsage(eventName: string, eventProperties?: { [key: string]: string }) {
|
||||
TelemetryAI.telemetryReporter.sendTelemetryEvent(eventName, eventProperties);
|
||||
}
|
||||
|
||||
public static runWithLatencyMeasure(functionToRun: () => void, eventName: string): void {
|
||||
const numberOfNanosecondsInSecond: number = 1000000000;
|
||||
const startTime: number = Number(process.hrtime.bigint());
|
||||
functionToRun();
|
||||
const latency: number = Number(process.hrtime.bigint()) - startTime;
|
||||
const measurement = {
|
||||
duration: latency / numberOfNanosecondsInSecond
|
||||
}
|
||||
TelemetryAI.telemetryReporter.sendTelemetryEvent(eventName, {}, measurement);
|
||||
}
|
||||
|
||||
private static telemetryReporter: TelemetryReporter;
|
||||
|
||||
constructor(vscodeContext: vscode.ExtensionContext) {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { ExtensionContext, Uri } from "vscode";
|
||||
import * as path from "path";
|
||||
import { USER_CODE_NAMES } from "./constants";
|
||||
|
||||
// tslint:disable-next-line: export-name
|
||||
export const getPathToScript = (
|
||||
context: ExtensionContext,
|
||||
folderName: string,
|
||||
fileName: string
|
||||
) => {
|
||||
const onDiskPath = Uri.file(
|
||||
path.join(context.extensionPath, folderName, fileName)
|
||||
);
|
||||
const scriptPath = onDiskPath.with({ scheme: "vscode-resource" });
|
||||
return scriptPath.fsPath;
|
||||
};
|
||||
|
||||
export const validCodeFileName = (filePath: string) => {
|
||||
return (
|
||||
filePath.endsWith(USER_CODE_NAMES.CODE_PY) ||
|
||||
filePath.endsWith(USER_CODE_NAMES.MAIN_PY)
|
||||
);
|
||||
};
|
Загрузка…
Ссылка в новой задаче