Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add screenshot command #17

Merged
merged 3 commits into from
Aug 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
## Unreleased
### Fixed
- Fix downloading projects with subdirectories
### Added
- Command to capture a screenshot from the remote device

## 0.2.0 - 2017-08-15
### Added
Expand Down
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
"title": "Open SSH Terminal",
"category": "ev3dev"
},
{
"command": "ev3devBrowser.captureScreenshot",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running from the command palette is actually broken here because there is no parameter passed as d in

vscode.commands.registerCommand('ev3devBrowser.captureScreenshot', d => ev3devBrowserProvider.captureScreenshot(d))

But some of the other commands are currently broken for the same reason, so we can leave this as is and fix them all later (I have a plan...).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just looked at this again and this command should not be showing up in the command palette. Not sure what's up. Perhaps a vscode bug.

"title": "Take Screenshot"
},
{
"command": "ev3devBrowser.remoteRun",
"title": "Run",
Expand Down Expand Up @@ -136,6 +140,10 @@
"command": "ev3devBrowser.openSshTerminal",
"when": "view == ev3devBrowser && viewItem == ev3devDevice"
},
{
"command": "ev3devBrowser.captureScreenshot",
"when": "view == ev3devBrowser && viewItem == ev3devDevice"
},
{
"command": "ev3devBrowser.remoteRun",
"when": "view == ev3devBrowser && viewItem == executableFile"
Expand Down Expand Up @@ -172,6 +180,7 @@
"@types/node": "^6.0.85",
"@types/ssh2": "~0.5.35",
"@types/ssh2-streams": "~0.1.2",
"@types/temp": "^0.8.3",
"mocha": "^2.3.3",
"typescript": "^2.4.2",
"vscode": "^1.0.0"
Expand All @@ -182,6 +191,7 @@
"dbus-native": "^0.2.2",
"dnode": "^1.2.2",
"ssh2": "~0.5.5",
"ssh2-streams": "~0.1.19"
"ssh2-streams": "~0.1.19",
"temp": "^0.8.3"
}
}
76 changes: 70 additions & 6 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import * as dnode from 'dnode';
import * as net from 'net'
import * as path from 'path'
import * as ssh2 from 'ssh2'
import * as ssh2Streams from 'ssh2-streams'
import * as fs from 'fs';
import * as net from 'net';

import * as path from 'path';
import * as ssh2 from 'ssh2';
import * as ssh2Streams from 'ssh2-streams';
import * as temp from 'temp';

import * as vscode from 'vscode';

import * as dnssd from './dnssd'
import * as dnssd from './dnssd';
import {
sanitizedDateString,
getSharedTempDir,
verifyFileHeader,
StatusBarProgressionMessage
} from './utils';

const S_IXUSR = parseInt('00100', 8);

Expand All @@ -31,6 +41,7 @@ export async function activate(context: vscode.ExtensionContext) : Promise<void>
ev3devBrowserProvider = new Ev3devBrowserProvider();
context.subscriptions.push(vscode.window.registerTreeDataProvider('ev3devBrowser', ev3devBrowserProvider));
context.subscriptions.push(vscode.commands.registerCommand('ev3devBrowser.openSshTerminal', d => ev3devBrowserProvider.openSshTerminal(d)));
context.subscriptions.push(vscode.commands.registerCommand('ev3devBrowser.captureScreenshot', d => ev3devBrowserProvider.captureScreenshot(d)));
context.subscriptions.push(vscode.commands.registerCommand('ev3devBrowser.deviceClicked', d => d.handleClick()));
context.subscriptions.push(vscode.commands.registerCommand('ev3devBrowser.fileClicked', f => f.handleClick()));
context.subscriptions.push(vscode.commands.registerCommand('ev3devBrowser.remoteRun', f => f.run()));
Expand All @@ -47,6 +58,9 @@ export function deactivate() {
device.destroy();
}
dnssdClient.destroy();

// The "temp" module should clean up automatically, but do this just in case.
temp.cleanupSync();
}

/**
Expand Down Expand Up @@ -198,6 +212,10 @@ class Ev3devBrowserProvider implements vscode.TreeDataProvider<Device | File> {
openSshTerminal(device: Device): void {
device.openSshTerminal();
}

captureScreenshot(device: Device): void {
device.captureScreenshot();
}

getTreeItem(element: Device | File): vscode.TreeItem {
return element;
Expand Down Expand Up @@ -521,6 +539,52 @@ class Device extends vscode.TreeItem {
[this.shellPort.toString()]);
term.show();
}

async captureScreenshot() {
const statusBarMessage = new StatusBarProgressionMessage("Attempting to capture screenshot...");

const handleCaptureError = e => {
vscode.window.showErrorMessage("Error capturing screenshot: " + (e.message || e));
statusBarMessage.finish();
}

try {
const screenshotDirectory = await getSharedTempDir('ev3dev-screenshots');
const screenshotBaseName = `ev3dev-${sanitizedDateString()}.png`;
const screenshotFile = `${screenshotDirectory}/${screenshotBaseName}`;

const conn = await this.exec('fbgrab -');
const writeStream = fs.createWriteStream(screenshotFile);

conn.on('error', (e: Error) => {
writeStream.removeAllListeners('finish');
handleCaptureError(e);
});

writeStream.on('open', () => {
conn.stdout.pipe(writeStream);
});

writeStream.on('error', (e: Error) => {
vscode.window.showErrorMessage("Error saving screenshot: " + e.message);
statusBarMessage.finish();
});

writeStream.on('finish', async () => {
const pngHeader = [ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ];
if (await verifyFileHeader(screenshotFile, pngHeader)) {
statusBarMessage.finish(`Screenshot "${screenshotBaseName}" successfully captured`);
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(screenshotFile));
}
else {
handleCaptureError("The screenshot was not in the correct format. You may need to upgrade to fbcat 0.5.0.");
}
});
}
catch (e) {
handleCaptureError(e);
}
}

iconPath = {
dark: path.join(resourceDir, 'icons', 'dark', 'yellow-circle.svg'),
Expand Down Expand Up @@ -626,7 +690,7 @@ class File extends vscode.TreeItem {
}
}

run() :void {
run(): void {
const command = `conrun -e ${this.path}`;
output.show(true);
output.clear();
Expand Down
111 changes: 111 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as vscode from 'vscode';
import * as temp from 'temp';
import * as fs from 'fs';
import { isArray } from 'util';

export function sanitizedDateString(date?: Date) {
const d = date || new Date();
const pad = (num: number) => ("00" + num).slice(-2);

return `${d.getFullYear()}-${pad(d.getMonth())}-${pad(d.getDay())}-${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nodejs doesn't have a built-in date formating function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not one for formatting it in a file-name-friendly way AFAIK. I've seen a fair number of custom packages to format dates which makes me think that there isn't a builtin for it.

}

const tempDirs: { [sharedKey: string]: string } = {};
export function getSharedTempDir(sharedKey: string): Promise<string> {
if (tempDirs[sharedKey]) {
return Promise.resolve(tempDirs[sharedKey]);
}

return new Promise((resolve, reject) => {
temp.track();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for style points, we could call temp.cleanupSync() when the extension exits (via deactivate())

It is possible to unload and reload the extension without the process actually exiting.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earlier I confirmed that it cleared the temp file properly when closing the window or reloading the session/switching folders.

It is possible to unload and reload the extension without the process actually exiting.

The main VSCode process doesn't exit, but the extension host does. Extensions are run in an independent sandboxed process which is restarted when you reload the window.

temp.mkdir(sharedKey, (err, dirPath) => {
if (err) {
reject(err);
}
else {
tempDirs[sharedKey] = dirPath;
resolve(dirPath);
}
})
});
}

export function openAndRead(path: string, offset: number, length: number, position: number): Promise<Buffer> {
return new Promise((resolve, reject) => {
fs.open(path, 'r', (err, fd) => {
if (err) {
reject(err);
return;
}

const buffer = new Buffer(length);
fs.read(fd, buffer, offset, length, position, (err, bytesRead, buffer) => {
if (err) {
reject(err);
return;
}

resolve(buffer);
});
});
})
}

export async function verifyFileHeader(filePath: string, expectedHeader: Buffer | number[], offset?: number): Promise<boolean> {
const bufferExpectedHeader = isArray(expectedHeader) ? new Buffer(<number[]>expectedHeader) : <Buffer>expectedHeader;
const header = await openAndRead(filePath, 0, bufferExpectedHeader.length, offset);
return header.compare(bufferExpectedHeader) == 0;
}

export class StatusBarProgressionMessage {
private statusBarItem: vscode.StatusBarItem;

constructor(initialMessage?: string) {
this.statusBarItem = vscode.window.createStatusBarItem();
if (initialMessage) {
this.statusBarItem.text = initialMessage;
this.statusBarItem.show();
}
}

/**
* Updates the displayed message.
* @param newMessage The new message to display
*/
public update(newMessage: string) {
if (!this.statusBarItem) {
return;
}

this.statusBarItem.text = newMessage;
this.statusBarItem.show();
}

/**
* Marks the progression as being finished. If a message is specified, it is
* shown temporarily before the item disappears.
*
* Note that a message should always be provided if an external message
* indicating a failure won't be presented to the user.
* @param finalMessage The last message to show for a short period
* @param delay The amount of time the final message should be shown
*/
public finish(finalMessage?: string, delay: number = 5000) {
if (!this.statusBarItem) {
return;
}

if (finalMessage) {
this.update(finalMessage);
setTimeout(() => this.dispose(), delay);
}
else {
this.dispose();
}
}

private dispose() {
this.statusBarItem.dispose();
this.statusBarItem = null;
}
}