ATL-302: Added built-in examples to the app.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta
2020-08-13 13:34:56 +02:00
committed by Akos Kitta
parent b5d7c3b45d
commit 1c9fcd0cdf
27 changed files with 728 additions and 101 deletions

View File

@@ -7,7 +7,7 @@ import { ILogger } from '@theia/core/lib/common/logger';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { LanguageServerContribution } from '@theia/languages/lib/node';
import { ArduinoLanguageServerContribution } from './language/arduino-language-server-contribution';
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
import { LibraryServiceServerPath, LibraryServiceServer, LibraryServiceClient } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { LibraryServiceImpl } from './library-service-impl';
import { BoardsServiceImpl } from './boards-service-impl';
@@ -35,6 +35,8 @@ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ArduinoEnvVariablesServer } from './arduino-env-variables-server';
import { NodeFileSystemExt } from './node-filesystem-ext';
import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext';
import { ExamplesServiceImpl } from './examples-service-impl';
import { ExamplesService, ExamplesServicePath } from '../common/protocol/examples-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(EnvVariablesServer).to(ArduinoEnvVariablesServer).inSingletonScope();
@@ -66,25 +68,31 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
})
).inSingletonScope();
// Shared examples service
bind(ExamplesServiceImpl).toSelf().inSingletonScope();
bind(ExamplesService).toService(ExamplesServiceImpl);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ExamplesServicePath, () => context.container.get(ExamplesService))).inSingletonScope();
// Language server
bind(ArduinoLanguageServerContribution).toSelf().inSingletonScope();
bind(LanguageServerContribution).toService(ArduinoLanguageServerContribution);
// Library service
const libraryServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(LibraryServiceImpl).toSelf().inSingletonScope();
bind(LibraryService).toService(LibraryServiceImpl);
bindBackendService(LibraryServicePath, LibraryService);
});
bind(ConnectionContainerModule).toConstantValue(libraryServiceConnectionModule);
bind(LibraryServiceImpl).toSelf().inSingletonScope();
bind(LibraryServiceServer).toService(LibraryServiceImpl);
bind(ConnectionHandler).toDynamicValue(context =>
new JsonRpcConnectionHandler<LibraryServiceClient>(LibraryServiceServerPath, client => {
const server = context.container.get<LibraryServiceImpl>(LibraryServiceImpl);
server.setClient(client);
client.onDidCloseConnection(() => server.dispose());
return server;
})
).inSingletonScope();
// Sketches service
const sketchesServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(SketchesServiceImpl).toSelf().inSingletonScope();
bind(SketchesService).toService(SketchesServiceImpl);
bindBackendService(SketchesServicePath, SketchesService);
});
bind(ConnectionContainerModule).toConstantValue(sketchesServiceConnectionModule);
// Shred sketches service
bind(SketchesServiceImpl).toSelf().inSingletonScope();
bind(SketchesService).toService(SketchesServiceImpl);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(SketchesServicePath, () => context.container.get(SketchesService))).inSingletonScope();
// Boards service
const boardsServiceConnectionModule = ConnectionContainerModule.create(async ({ bind, bindBackendService }) => {
@@ -190,6 +198,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// File-system extension for mapping paths to URIs
bind(NodeFileSystemExt).toSelf().inSingletonScope();
bind(FileSystemExt).toDynamicValue(context => context.container.get(NodeFileSystemExt));
bind(FileSystemExt).toService(NodeFileSystemExt);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(FileSystemExtPath, () => context.container.get(FileSystemExt))).inSingletonScope();
});

View File

@@ -112,8 +112,8 @@ export class BoardsServiceImpl implements BoardsService {
if (this.discoveryTimer !== undefined) {
clearInterval(this.discoveryTimer);
}
this.logger.info('<<< Disposed boards service.');
this.client = undefined;
this.logger.info('<<< Disposed boards service.');
}
async getAttachedBoards(): Promise<Board[]> {
@@ -370,15 +370,15 @@ export class BoardsServiceImpl implements BoardsService {
}
async install(options: { item: BoardsPackage, version?: Installable.Version }): Promise<void> {
const pkg = options.item;
const version = !!options.version ? options.version : pkg.availableVersions[0];
const item = options.item;
const version = !!options.version ? options.version : item.availableVersions[0];
const coreClient = await this.coreClientProvider.client();
if (!coreClient) {
return;
}
const { client, instance } = coreClient;
const [platform, architecture] = pkg.id.split(':');
const [platform, architecture] = item.id.split(':');
const req = new PlatformInstallReq();
req.setInstance(instance);
@@ -386,7 +386,7 @@ export class BoardsServiceImpl implements BoardsService {
req.setPlatformPackage(platform);
req.setVersion(version);
console.info('Starting board installation', pkg);
console.info('>>> Starting boards package installation...', item);
const resp = client.platformInstall(req);
resp.on('data', (r: PlatformInstallResp) => {
const prog = r.getProgress();
@@ -399,34 +399,34 @@ export class BoardsServiceImpl implements BoardsService {
resp.on('error', reject);
});
if (this.client) {
const packages = await this.search({});
const updatedPackage = packages.find(({ id }) => id === pkg.id) || pkg;
this.client.notifyBoardInstalled({ pkg: updatedPackage });
const items = await this.search({});
const updated = items.find(other => BoardsPackage.equals(other, item)) || item;
this.client.notifyInstalled({ item: updated });
}
console.info('Board installation done', pkg);
console.info('<<< Boards package installation done.', item);
}
async uninstall(options: { item: BoardsPackage }): Promise<void> {
const pkg = options.item;
const item = options.item;
const coreClient = await this.coreClientProvider.client();
if (!coreClient) {
return;
}
const { client, instance } = coreClient;
const [platform, architecture] = pkg.id.split(':');
const [platform, architecture] = item.id.split(':');
const req = new PlatformUninstallReq();
req.setInstance(instance);
req.setArchitecture(architecture);
req.setPlatformPackage(platform);
console.info('Starting board uninstallation', pkg);
console.info('>>> Starting boards package uninstallation...', item);
let logged = false;
const resp = client.platformUninstall(req);
resp.on('data', (_: PlatformUninstallResp) => {
if (!logged) {
this.toolOutputService.append({ tool: 'board uninstall', chunk: `uninstalling ${pkg.id}\n` });
this.toolOutputService.append({ tool: 'board uninstall', chunk: `uninstalling ${item.id}\n` });
logged = true;
}
})
@@ -435,10 +435,10 @@ export class BoardsServiceImpl implements BoardsService {
resp.on('error', reject);
});
if (this.client) {
// Here, unlike at `install` we send out the argument `pkg`. Otherwise, we would not know about the board FQBN.
this.client.notifyBoardUninstalled({ pkg });
// Here, unlike at `install` we send out the argument `item`. Otherwise, we would not know about the board FQBN.
this.client.notifyUninstalled({ item });
}
console.info('Board uninstallation done', pkg);
console.info('<<< Boards package uninstallation done.', item);
}
}

View File

@@ -15,11 +15,16 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
protected readonly toolOutputService: ToolOutputServiceServer;
protected readonly onIndexUpdatedEmitter = new Emitter<void>();
protected readonly onClientReadyEmitter = new Emitter<void>();
get onIndexUpdated(): Event<void> {
return this.onIndexUpdatedEmitter.event;
}
get onClientReady(): Event<void> {
return this.onClientReadyEmitter.event;
}
close(client: CoreClientProvider.Client): void {
client.client.close();
}
@@ -28,10 +33,12 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
if (port && port === this._port) {
// No need to create a new gRPC client, but we have to update the indexes.
if (this._client) {
this.updateIndexes(this._client);
await this.updateIndexes(this._client);
this.onClientReadyEmitter.fire();
}
} else {
return super.reconcileClient(port);
await super.reconcileClient(port);
this.onClientReadyEmitter.fire();
}
}

View File

@@ -0,0 +1,79 @@
import { inject, injectable, postConstruct } from 'inversify';
import { join, basename } from 'path';
import * as fs from './fs-extra';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { Sketch } from '../common/protocol/sketches-service';
import { SketchesServiceImpl } from './sketches-service-impl';
import { ExamplesService, ExampleContainer } from '../common/protocol/examples-service';
@injectable()
export class ExamplesServiceImpl implements ExamplesService {
@inject(SketchesServiceImpl)
protected readonly sketchesService: SketchesServiceImpl;
protected _all: ExampleContainer | undefined;
@postConstruct()
protected init(): void {
this.all();
}
async all(): Promise<ExampleContainer> {
if (this._all) {
return this._all;
}
this._all = await this.load();
return this._all;
}
protected async load(path: string = join(__dirname, '..', '..', 'Examples')): Promise<ExampleContainer> {
if (!await fs.exists(path)) {
throw new Error('Examples are not available');
}
const stat = await fs.stat(path);
if (!stat.isDirectory) {
throw new Error(`${path} is not a directory.`);
}
const names = await fs.readdir(path);
const sketches: Sketch[] = [];
const children: ExampleContainer[] = [];
for (const p of names.map(name => join(path, name))) {
const stat = await fs.stat(p);
if (stat.isDirectory()) {
const sketch = await this.tryLoadSketch(p);
if (sketch) {
sketches.push(sketch);
} else {
const child = await this.load(p);
children.push(child);
}
}
}
const label = basename(path);
return {
label,
children,
sketches
};
}
protected async group(paths: string[]): Promise<Map<string, fs.Stats>> {
const map = new Map<string, fs.Stats>();
for (const path of paths) {
const stat = await fs.stat(path);
map.set(path, stat);
}
return map;
}
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchesService.loadSketch(FileUri.create(path).toString());
return sketch;
} catch {
return undefined;
}
}
}

View File

@@ -2,6 +2,7 @@ import * as fs from 'fs';
import { promisify } from 'util';
export const constants = fs.constants;
export type Stats = fs.Stats;
export const existsSync = fs.existsSync;
export const lstatSync = fs.lstatSync;

View File

@@ -1,5 +1,5 @@
import { injectable, inject } from 'inversify';
import { Library, LibraryService } from '../common/protocol/library-service';
import { injectable, inject, postConstruct } from 'inversify';
import { LibraryPackage, LibraryService, LibraryServiceClient } from '../common/protocol/library-service';
import { CoreClientProvider } from './core-client-provider';
import {
LibrarySearchReq,
@@ -15,17 +15,37 @@ import {
} from './cli-protocol/commands/lib_pb';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
import { Installable } from '../common/protocol/installable';
import { ILogger, notEmpty } from '@theia/core';
import { Deferred } from '@theia/core/lib/common/promise-util';
@injectable()
export class LibraryServiceImpl implements LibraryService {
@inject(ILogger)
protected logger: ILogger;
@inject(CoreClientProvider)
protected readonly coreClientProvider: CoreClientProvider;
@inject(ToolOutputServiceServer)
protected readonly toolOutputService: ToolOutputServiceServer;
async search(options: { query?: string }): Promise<Library[]> {
protected ready = new Deferred<void>();
protected client: LibraryServiceClient | undefined;
@postConstruct()
protected init(): void {
this.coreClientProvider.client().then(client => {
if (client) {
this.ready.resolve();
} else {
this.coreClientProvider.onClientReady(() => this.ready.resolve());
}
})
}
async search(options: { query?: string }): Promise<LibraryPackage[]> {
await this.ready.promise;
const coreClient = await this.coreClientProvider.client();
if (!coreClient) {
return [];
@@ -71,9 +91,74 @@ export class LibraryServiceImpl implements LibraryService {
return items;
}
async install(options: { item: Library, version?: Installable.Version }): Promise<void> {
const library = options.item;
const version = !!options.version ? options.version : library.availableVersions[0];
async list({ fqbn }: { fqbn?: string | undefined }): Promise<LibraryPackage[]> {
await this.ready.promise;
const coreClient = await this.coreClientProvider.client();
if (!coreClient) {
return [];
}
const { client, instance } = coreClient;
const req = new LibraryListReq();
req.setInstance(instance);
req.setAll(true);
const resp = await new Promise<LibraryListResp>((resolve, reject) => client.libraryList(req, ((error, resp) => !!error ? reject(error) : resolve(resp))));
const x = resp.getInstalledLibraryList().map(item => {
const release = item.getRelease();
const library = item.getLibrary();
if (!release || !library) {
return undefined;
}
// https://arduino.github.io/arduino-cli/latest/rpc/commands/#librarylocation
// 0: In the libraries subdirectory of the Arduino IDE installation. (`ide_builtin`)
// 1: In the libraries subdirectory of the user directory (sketchbook). (`user`)
// 2: In the libraries subdirectory of a platform. (`platform_builtin`)
// 3: When LibraryLocation is used in a context where a board is specified, this indicates the library is
// in the libraries subdirectory of a platform referenced by the board's platform. (`referenced_platform_builtin`)
// If 0, we ignore it.
// If 1, we include always.
// If 2, we include iff `fqbn` is specified and the platform matches.
// if 3, TODO
const location = library.getLocation();
if (location === 0) {
return undefined;
}
if (location === 2) {
if (!fqbn) {
return undefined;
}
const architectures = library.getArchitecturesList();
const [platform] = library.getContainerPlatform().split(':');
if (!platform) {
return undefined;
}
const [boardPlatform, boardArchitecture] = fqbn.split(':');
if (boardPlatform !== platform || architectures.indexOf(boardArchitecture) === -1) {
return undefined;
}
}
const installedVersion = library.getVersion();
return toLibrary({
name: library.getName(),
installedVersion,
installable: true,
description: library.getSentence(),
summary: library.getParagraph(),
includes: release.getProvidesIncludesList(),
moreInfoLink: library.getWebsite()
}, release, [library.getVersion()]);
}).filter(notEmpty);
console.log(x);
return x;
}
async install(options: { item: LibraryPackage, version?: Installable.Version }): Promise<void> {
await this.ready.promise;
const item = options.item;
const version = !!options.version ? options.version : item.availableVersions[0];
const coreClient = await this.coreClientProvider.client();
if (!coreClient) {
return;
@@ -82,9 +167,10 @@ export class LibraryServiceImpl implements LibraryService {
const req = new LibraryInstallReq();
req.setInstance(instance);
req.setName(library.name);
req.setName(item.name);
req.setVersion(version);
console.info('>>> Starting library package installation...', item);
const resp = client.libraryInstall(req);
resp.on('data', (r: LibraryInstallResp) => {
const prog = r.getProgress();
@@ -96,10 +182,18 @@ export class LibraryServiceImpl implements LibraryService {
resp.on('end', resolve);
resp.on('error', reject);
});
if (this.client) {
const items = await this.search({});
const updated = items.find(other => LibraryPackage.equals(other, item)) || item;
this.client.notifyInstalled({ item: updated });
}
console.info('<<< Library package installation done.', item);
}
async uninstall(options: { item: Library }): Promise<void> {
const library = options.item;
async uninstall(options: { item: LibraryPackage }): Promise<void> {
const item = options.item;
const coreClient = await this.coreClientProvider.client();
if (!coreClient) {
return;
@@ -108,14 +202,15 @@ export class LibraryServiceImpl implements LibraryService {
const req = new LibraryUninstallReq();
req.setInstance(instance);
req.setName(library.name);
req.setVersion(library.installedVersion!);
req.setName(item.name);
req.setVersion(item.installedVersion!);
console.info('>>> Starting library package uninstallation...', item);
let logged = false;
const resp = client.libraryUninstall(req);
resp.on('data', (_: LibraryUninstallResp) => {
if (!logged) {
this.toolOutputService.append({ tool: 'library', chunk: `uninstalling ${library.name}:${library.installedVersion}%\n` });
this.toolOutputService.append({ tool: 'library', chunk: `uninstalling ${item.name}:${item.installedVersion}%\n` });
logged = true;
}
});
@@ -123,11 +218,25 @@ export class LibraryServiceImpl implements LibraryService {
resp.on('end', resolve);
resp.on('error', reject);
});
if (this.client) {
this.client.notifyUninstalled({ item });
}
console.info('<<< Library package uninstallation done.', item);
}
setClient(client: LibraryServiceClient | undefined): void {
this.client = client;
}
dispose(): void {
this.logger.info('>>> Disposing library service...');
this.client = undefined;
this.logger.info('<<< Disposed library service.');
}
}
function toLibrary(tpl: Partial<Library>, release: LibraryRelease, availableVersions: string[]): Library {
function toLibrary(tpl: Partial<LibraryPackage>, release: LibraryRelease, availableVersions: string[]): LibraryPackage {
return {
name: '',
installable: false,
@@ -135,6 +244,7 @@ function toLibrary(tpl: Partial<Library>, release: LibraryRelease, availableVers
author: release.getAuthor(),
availableVersions,
includes: release.getProvidesIncludesList(),
description: release.getSentence(),
moreInfoLink: release.getWebsite(),
summary: release.getParagraph()

View File

@@ -19,6 +19,8 @@ const MAX_FILESYSTEM_DEPTH = 40;
const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/;
const prefix = '.arduinoProIDE-unsaved';
// TODO: `fs`: use async API
@injectable()
export class SketchesServiceImpl implements SketchesService, BackendApplicationContribution {
@@ -205,6 +207,22 @@ export class SketchesServiceImpl implements SketchesService, BackendApplicationC
}
}
async cloneExample(uri: string): Promise<Sketch> {
const sketch = await this.loadSketch(uri);
const parentPath = await new Promise<string>((resolve, reject) => {
this.temp.mkdir({ prefix }, (err, dirPath) => {
if (err) {
reject(err);
return;
}
resolve(dirPath);
})
});
const destinationUri = FileUri.create(path.join(parentPath, sketch.name)).toString();
const copiedSketchUri = await this.copy(sketch, { destinationUri });
return this.loadSketch(copiedSketchUri);
}
protected async simpleLocalWalk(
root: string,
maxDepth: number,
@@ -258,15 +276,15 @@ export class SketchesServiceImpl implements SketchesService, BackendApplicationC
async createNewSketch(): Promise<Sketch> {
const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const today = new Date();
const parent = await new Promise<string>((resolve, reject) => {
this.temp.mkdir({ prefix: '.arduinoProIDE-unsaved' }, (err, dirPath) => {
const parentPath = await new Promise<string>((resolve, reject) => {
this.temp.mkdir({ prefix }, (err, dirPath) => {
if (err) {
reject(err);
return;
}
resolve(dirPath);
})
})
});
});
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`;
const config = await this.configService.getConfiguration();
const user = FileUri.fsPath(config.sketchDirUri);
@@ -286,7 +304,7 @@ export class SketchesServiceImpl implements SketchesService, BackendApplicationC
throw new Error('Cannot create a unique sketch name');
}
const sketchDir = path.join(parent, sketchName)
const sketchDir = path.join(parentPath, sketchName)
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
await fs.mkdirp(sketchDir);
await fs.writeFile(sketchFile, `void setup() {
@@ -346,7 +364,7 @@ void loop() {
temp = firstToLowerCase(temp);
}
}
return sketchPath.indexOf('.arduinoProIDE-unsaved') !== -1 && sketchPath.startsWith(temp);
return sketchPath.indexOf(prefix) !== -1 && sketchPath.startsWith(temp);
}
async copy(sketch: Sketch, { destinationUri }: { destinationUri: string }): Promise<string> {