mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-06-06 04:06:32 +00:00
Reimplemented sketchbook watcher.
Moved it to the frontend. Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
parent
911875665d
commit
b1ab6df8b7
@ -1,136 +0,0 @@
|
||||
{
|
||||
"$id": "http://arduino.cc/arduino-cli.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Arduino CLI Configuration",
|
||||
"properties": {
|
||||
"board_manager": {
|
||||
"type": "object",
|
||||
"description": "Board Manager Configuration",
|
||||
"properties": {
|
||||
"additional_urls": {
|
||||
"type": "array",
|
||||
"description": "If your board requires 3rd party core packages to work, you can list the URLs to additional package indexes in the Arduino CLI configuration file.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "URL pointing to the 3rd party core package index JSON.",
|
||||
"pattern": "^(.*)$"
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"daemon": {
|
||||
"type": "object",
|
||||
"description": "CLI Daemon Configuration",
|
||||
"properties": {
|
||||
"port": {
|
||||
"type": [
|
||||
"string",
|
||||
"number"
|
||||
],
|
||||
"description": "The CLI daemon port where the gRPC clients can connect to.",
|
||||
"pattern": "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"directories": {
|
||||
"type": "object",
|
||||
"description": "Directories Configuration",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Path to the the data folder where core packages will be stored.",
|
||||
"pattern": "^(.*)$"
|
||||
},
|
||||
"downloads": {
|
||||
"type": "string",
|
||||
"description": "Path to the staging folder.",
|
||||
"pattern": "^(.*)$"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "Path to the sketchbooks.",
|
||||
"pattern": "^(.*)$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"description": "Logging Configuration",
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "Path to the file where logs will be written.",
|
||||
"pattern": "^(.*)$"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "The output format for the logs, can be 'text' or 'json'",
|
||||
"enum": [
|
||||
"text",
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"level": {
|
||||
"type": "string",
|
||||
"description": "Messages with this level and above will be logged.",
|
||||
"enum": [
|
||||
"trace",
|
||||
"debug",
|
||||
"info",
|
||||
"warning",
|
||||
"error",
|
||||
"fatal",
|
||||
"panic"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"sketch": {
|
||||
"type": "object",
|
||||
"description": "Sketch Configuration",
|
||||
"properties": {
|
||||
"always_export_binaries": {
|
||||
"type": "boolean",
|
||||
"description": "Controls whether the compiled binaries will be exported into the sketch's 'build' folder or not. 'false' if the binaries are not exported."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"metrics": {
|
||||
"type": "object",
|
||||
"description": "Metrics Configuration",
|
||||
"properties": {
|
||||
"addr": {
|
||||
"type": "string",
|
||||
"description": "Address to the metrics endpoint. Must be a full address with host, address, and port. For instance, ':9090' represents 'localhost:9090'",
|
||||
"pattern": "^(.*)$"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the metrics is enabled or not."
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"library": {
|
||||
"type": "object",
|
||||
"description": "Library Configuration",
|
||||
"properties": {
|
||||
"enable_unsafe_install": {
|
||||
"type": "boolean",
|
||||
"description": "Set to 'true' to enable the use of the '--git-url' and '--zip-file' flags with 'arduino-cli lib install' These are considered 'unsafe' installation methods because they allow installing files that have not passed through the Library Manager submission process."
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
@ -181,6 +181,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// Sketch list service
|
||||
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
|
||||
bind(SketchesServiceClientImpl).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(SketchesServiceClientImpl);
|
||||
|
||||
// Config service
|
||||
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
|
||||
|
@ -28,7 +28,7 @@ export class Sketchbook extends SketchContribution {
|
||||
this.register(sketches);
|
||||
this.mainMenuManager.update();
|
||||
});
|
||||
this.notificationCenter.onSketchbookChanged(({ created, removed }) => {
|
||||
this.sketchServiceClient.onSketchbookDidChange(({ created, removed }) => {
|
||||
this.unregister(removed);
|
||||
this.register(created);
|
||||
this.mainMenuManager.update();
|
||||
|
@ -21,7 +21,6 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
|
||||
protected readonly libraryInstalledEmitter = new Emitter<{ item: LibraryPackage }>();
|
||||
protected readonly libraryUninstalledEmitter = new Emitter<{ item: LibraryPackage }>();
|
||||
protected readonly attachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
|
||||
protected readonly sketchbookChangedEmitter = new Emitter<{ created: Sketch[], removed: Sketch[] }>();
|
||||
protected readonly recentSketchesChangedEmitter = new Emitter<{ sketches: Sketch[] }>();
|
||||
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
@ -33,8 +32,7 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
|
||||
this.platformUninstalledEmitter,
|
||||
this.libraryInstalledEmitter,
|
||||
this.libraryUninstalledEmitter,
|
||||
this.attachedBoardsChangedEmitter,
|
||||
this.sketchbookChangedEmitter
|
||||
this.attachedBoardsChangedEmitter
|
||||
);
|
||||
|
||||
readonly onIndexUpdated = this.indexUpdatedEmitter.event;
|
||||
@ -46,7 +44,6 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
|
||||
readonly onLibraryInstalled = this.libraryInstalledEmitter.event;
|
||||
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
|
||||
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
|
||||
readonly onSketchbookChanged = this.sketchbookChangedEmitter.event;
|
||||
readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event;
|
||||
|
||||
@postConstruct()
|
||||
@ -94,10 +91,6 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
|
||||
this.attachedBoardsChangedEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifySketchbookChanged(event: { created: Sketch[], removed: Sketch[] }): void {
|
||||
this.sketchbookChangedEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
|
||||
this.recentSketchesChangedEmitter.fire(event);
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ export interface ConfigService {
|
||||
getVersion(): Promise<Readonly<{ version: string, commit: string, status?: string }>>;
|
||||
getConfiguration(): Promise<Config>;
|
||||
setConfiguration(config: Config): Promise<void>;
|
||||
getConfigurationFileSchemaUri(): Promise<string>;
|
||||
isInDataDir(uri: string): Promise<boolean>;
|
||||
isInSketchDir(uri: string): Promise<boolean>;
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ export interface NotificationServiceClient {
|
||||
notifyLibraryInstalled(event: { item: LibraryPackage }): void;
|
||||
notifyLibraryUninstalled(event: { item: LibraryPackage }): void;
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
|
||||
notifySketchbookChanged(event: { created: Sketch[], removed: Sketch[] }): void;
|
||||
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void;
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,13 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { Sketch, SketchesService } from '../../common/protocol';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { ConfigService } from './config-service';
|
||||
import { DisposableCollection, Emitter } from '@theia/core';
|
||||
import { FileChangeType } from '@theia/filesystem/lib/browser';
|
||||
|
||||
@injectable()
|
||||
export class SketchesServiceClientImpl {
|
||||
export class SketchesServiceClientImpl implements FrontendApplicationContribution {
|
||||
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
@ -21,6 +25,58 @@ export class SketchesServiceClientImpl {
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
protected toDispose = new DisposableCollection();
|
||||
protected sketches = new Map<string, Sketch>();
|
||||
protected sketchbookDidChangeEmitter = new Emitter<{ created: Sketch[], removed: Sketch[] }>();
|
||||
readonly onSketchbookDidChange = this.sketchbookDidChangeEmitter.event;
|
||||
|
||||
onStart(): void {
|
||||
this.configService.getConfiguration().then(({ sketchDirUri }) => {
|
||||
this.sketchService.getSketches(sketchDirUri).then(sketches => {
|
||||
const sketchbookUri = new URI(sketchDirUri);
|
||||
for (const sketch of sketches) {
|
||||
this.sketches.set(sketch.uri, sketch);
|
||||
}
|
||||
this.toDispose.push(this.fileService.watch(new URI(sketchDirUri), { recursive: true, excludes: [] }));
|
||||
this.toDispose.push(this.fileService.onDidFilesChange(async event => {
|
||||
for (const { type, resource } of event.changes) {
|
||||
// We track main sketch files changes only.
|
||||
if (sketchbookUri.isEqualOrParent(resource)) {
|
||||
const { ext } = resource.path; // TODO: add support for `.pde`.
|
||||
if (ext === '.ino') {
|
||||
if (type === FileChangeType.ADDED) {
|
||||
try {
|
||||
const toAdd = await this.sketchService.loadSketch(resource.parent.toString());
|
||||
if (!this.sketches.has(toAdd.uri)) {
|
||||
console.log(`New sketch '${toAdd.name}' was crated in sketchbook '${sketchDirUri}'.`);
|
||||
this.sketches.set(toAdd.uri, toAdd);
|
||||
this.fireSoon(toAdd, 'created');
|
||||
}
|
||||
} catch { }
|
||||
} else if (type === FileChangeType.DELETED) {
|
||||
const uri = resource.parent.toString();
|
||||
const toDelete = this.sketches.get(uri);
|
||||
if (toDelete) {
|
||||
console.log(`Sketch '${toDelete.name}' was removed from sketchbook '${sketchbookUri}'.`);
|
||||
this.sketches.delete(uri);
|
||||
this.fireSoon(toDelete, 'removed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
async currentSketch(): Promise<Sketch | undefined> {
|
||||
const sketches = (await Promise.all(this.workspaceService.tryGetRoots().map(({ resource }) => this.sketchService.getSketchFolder(resource.toString())))).filter(notEmpty);
|
||||
if (!sketches.length) {
|
||||
@ -46,4 +102,31 @@ export class SketchesServiceClientImpl {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private fireSoonHandle?: number;
|
||||
private bufferedSketchbookEvents: { type: 'created' | 'removed', sketch: Sketch }[] = [];
|
||||
|
||||
private fireSoon(sketch: Sketch, type: 'created' | 'removed'): void {
|
||||
this.bufferedSketchbookEvents.push({ type, sketch });
|
||||
|
||||
if (typeof this.fireSoonHandle === 'number') {
|
||||
window.clearTimeout(this.fireSoonHandle);
|
||||
}
|
||||
|
||||
this.fireSoonHandle = window.setTimeout(() => {
|
||||
const event: { created: Sketch[], removed: Sketch[] } = {
|
||||
created: [],
|
||||
removed: []
|
||||
};
|
||||
for (const { type, sketch } of this.bufferedSketchbookEvents) {
|
||||
if (type === 'created') {
|
||||
event.created.push(sketch);
|
||||
} else {
|
||||
event.removed.push(sketch);
|
||||
}
|
||||
}
|
||||
this.sketchbookDidChangeEmitter.fire(event);
|
||||
this.bufferedSketchbookEvents.length = 0;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ import { MonitorClientProvider } from './monitor/monitor-client-provider';
|
||||
import { ConfigServiceImpl } from './config-service-impl';
|
||||
import { HostedPluginReader } from './theia/plugin-ext/plugin-reader';
|
||||
import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader';
|
||||
import { ConfigFileValidator } from './config-file-validator';
|
||||
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
|
||||
import { NodeFileSystemExt } from './node-filesystem-ext';
|
||||
@ -43,7 +42,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
rebind(TheiaBackendApplication).toService(BackendApplication);
|
||||
|
||||
// Shared config service
|
||||
bind(ConfigFileValidator).toSelf().inSingletonScope();
|
||||
bind(ConfigServiceImpl).toSelf().inSingletonScope();
|
||||
bind(ConfigService).toService(ConfigServiceImpl);
|
||||
// Note: The config service must start earlier than the daemon, hence the binding order of the BA contribution does matter.
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { join } from 'path';
|
||||
import { RecursivePartial } from '@theia/core/lib/common/types';
|
||||
|
||||
export const CLI_CONFIG = 'arduino-cli.yaml';
|
||||
export const CLI_CONFIG_SCHEMA = 'arduino-cli.schema.json';
|
||||
export const CLI_CONFIG_SCHEMA_PATH = join(__dirname, '..', '..', 'data', 'cli', 'schema', CLI_CONFIG_SCHEMA);
|
||||
|
||||
export interface BoardManager {
|
||||
readonly additional_urls: Array<string>;
|
||||
|
@ -1,60 +0,0 @@
|
||||
import * as Ajv from 'ajv';
|
||||
import * as fs from './fs-extra';
|
||||
import { injectable } from 'inversify';
|
||||
import { CLI_CONFIG_SCHEMA_PATH, DefaultCliConfig } from './cli-config';
|
||||
|
||||
@injectable()
|
||||
export class ConfigFileValidator {
|
||||
|
||||
protected readonly function = new Ajv().compile(JSON.parse(fs.readFileSync(CLI_CONFIG_SCHEMA_PATH, 'utf8')));
|
||||
|
||||
async validate(pathOrObject: string | object): Promise<boolean> {
|
||||
return this.doValidate(typeof pathOrObject === 'string' ? fs.readFileSync(pathOrObject) : pathOrObject);
|
||||
}
|
||||
|
||||
protected async doValidate(object: object): Promise<boolean> {
|
||||
const valid = this.function(object);
|
||||
if (!valid) {
|
||||
if (Array.isArray(this.function.errors)) {
|
||||
for (const error of this.function.errors) {
|
||||
console.log(JSON.stringify(error));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!DefaultCliConfig.is(object)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { directories: { data, downloads, user } } = object;
|
||||
for (const path of [data, downloads, user]) {
|
||||
const validPath = await this.isValidPath(path);
|
||||
if (!validPath) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const port = typeof object.daemon.port === 'string' ? Number.parseInt(object.daemon.port, 10) : object.daemon.port;
|
||||
if (Number.isNaN(port) || port <= 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async isValidPath(path: string): Promise<boolean> {
|
||||
try {
|
||||
if (!path.trim()) {
|
||||
return false;
|
||||
}
|
||||
const exists = await fs.exists(path);
|
||||
if (!exists) {
|
||||
await fs.mkdirp(path);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as temp from 'temp';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { promisify } from 'util';
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import * as deepmerge from 'deepmerge';
|
||||
import { injectable, inject, named } from 'inversify';
|
||||
@ -10,14 +12,12 @@ import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
||||
import { ConfigService, Config, NotificationServiceServer } from '../common/protocol';
|
||||
import * as fs from './fs-extra';
|
||||
import { spawnCommand } from './exec-util';
|
||||
import { RawData, WriteRequest } from './cli-protocol/settings/settings_pb';
|
||||
import { SettingsClient } from './cli-protocol/settings/settings_grpc_pb';
|
||||
import * as serviceGrpcPb from './cli-protocol/settings/settings_grpc_pb';
|
||||
import { ConfigFileValidator } from './config-file-validator';
|
||||
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
|
||||
import { DefaultCliConfig, CLI_CONFIG_SCHEMA_PATH, CLI_CONFIG } from './cli-config';
|
||||
import { DefaultCliConfig, CLI_CONFIG } from './cli-config';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { deepClone } from '@theia/core';
|
||||
@ -34,9 +34,6 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariablesServer: EnvVariablesServer;
|
||||
|
||||
@inject(ConfigFileValidator)
|
||||
protected readonly validator: ConfigFileValidator;
|
||||
|
||||
@inject(ArduinoDaemonImpl)
|
||||
protected readonly daemon: ArduinoDaemonImpl;
|
||||
|
||||
@ -67,10 +64,6 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
return new URI(configDirUri).resolve(CLI_CONFIG).toString();
|
||||
}
|
||||
|
||||
async getConfigurationFileSchemaUri(): Promise<string> {
|
||||
return FileUri.create(CLI_CONFIG_SCHEMA_PATH).toString();
|
||||
}
|
||||
|
||||
async getConfiguration(): Promise<Config> {
|
||||
await this.ready.promise;
|
||||
return this.config;
|
||||
@ -129,7 +122,7 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
const cliConfigFileUri = await this.getCliConfigFileUri();
|
||||
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
|
||||
try {
|
||||
const content = await fs.readFile(cliConfigPath, { encoding: 'utf8' });
|
||||
const content = await promisify(fs.readFile)(cliConfigPath, { encoding: 'utf8' });
|
||||
const model = yaml.safeLoad(content) || {};
|
||||
// The CLI can run with partial (missing `port`, `directories`), the app cannot, we merge the default with the user's config.
|
||||
const fallbackModel = await this.getFallbackCliConfig();
|
||||
@ -152,7 +145,7 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
});
|
||||
});
|
||||
await spawnCommand(`"${cliPath}"`, ['config', 'init', '--dest-dir', `"${throwawayDirPath}"`]);
|
||||
const rawYaml = await fs.readFile(path.join(throwawayDirPath, CLI_CONFIG), { encoding: 'utf-8' });
|
||||
const rawYaml = await promisify(fs.readFile)(path.join(throwawayDirPath, CLI_CONFIG), { encoding: 'utf-8' });
|
||||
const model = yaml.safeLoad(rawYaml.trim());
|
||||
return model as DefaultCliConfig;
|
||||
}
|
||||
@ -160,10 +153,10 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
protected async ensureCliConfigExists(): Promise<void> {
|
||||
const cliConfigFileUri = await this.getCliConfigFileUri();
|
||||
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
|
||||
let exists = await fs.exists(cliConfigPath);
|
||||
let exists = await promisify(fs.exists)(cliConfigPath);
|
||||
if (!exists) {
|
||||
await this.initCliConfigTo(path.dirname(cliConfigPath));
|
||||
exists = await fs.exists(cliConfigPath);
|
||||
exists = await promisify(fs.exists)(cliConfigPath);
|
||||
if (!exists) {
|
||||
throw new Error(`Could not initialize the default CLI configuration file at ${cliConfigPath}.`);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { join, basename } from 'path';
|
||||
import * as fs from './fs-extra';
|
||||
import * as fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { Sketch } from '../common/protocol/sketches-service';
|
||||
@ -33,7 +34,7 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
return this._all;
|
||||
}
|
||||
const exampleRootPath = join(__dirname, '..', '..', 'Examples');
|
||||
const exampleNames = await fs.readdir(exampleRootPath);
|
||||
const exampleNames = await promisify(fs.readdir)(exampleRootPath);
|
||||
this._all = await Promise.all(exampleNames.map(name => join(exampleRootPath, name)).map(path => this.load(path)));
|
||||
return this._all;
|
||||
}
|
||||
@ -70,15 +71,15 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
if (installDirUri) {
|
||||
for (const example of ['example', 'Example', 'EXAMPLE', 'examples', 'Examples', 'EXAMPLES']) {
|
||||
const examplesPath = join(FileUri.fsPath(installDirUri), example);
|
||||
const exists = await fs.exists(examplesPath);
|
||||
const isDir = exists && (await fs.lstat(examplesPath)).isDirectory();
|
||||
const exists = await promisify(fs.exists)(examplesPath);
|
||||
const isDir = exists && (await promisify(fs.lstat)(examplesPath)).isDirectory();
|
||||
if (isDir) {
|
||||
const fileNames = await fs.readdir(examplesPath);
|
||||
const fileNames = await promisify(fs.readdir)(examplesPath);
|
||||
const children: ExampleContainer[] = [];
|
||||
const sketches: Sketch[] = [];
|
||||
for (const fileName of fileNames) {
|
||||
const subPath = join(examplesPath, fileName);
|
||||
const subIsDir = (await fs.lstat(subPath)).isDirectory();
|
||||
const subIsDir = (await promisify(fs.lstat)(subPath)).isDirectory();
|
||||
if (subIsDir) {
|
||||
const sketch = await this.tryLoadSketch(subPath);
|
||||
if (!sketch) {
|
||||
@ -109,18 +110,18 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
|
||||
// Built-ins are included inside the IDE.
|
||||
protected async load(path: string): Promise<ExampleContainer> {
|
||||
if (!await fs.exists(path)) {
|
||||
if (!await promisify(fs.exists)(path)) {
|
||||
throw new Error('Examples are not available');
|
||||
}
|
||||
const stat = await fs.stat(path);
|
||||
const stat = await promisify(fs.stat)(path);
|
||||
if (!stat.isDirectory) {
|
||||
throw new Error(`${path} is not a directory.`);
|
||||
}
|
||||
const names = await fs.readdir(path);
|
||||
const names = await promisify(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);
|
||||
const stat = await promisify(fs.stat)(p);
|
||||
if (stat.isDirectory()) {
|
||||
const sketch = await this.tryLoadSketch(p);
|
||||
if (sketch) {
|
||||
@ -142,7 +143,7 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
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);
|
||||
const stat = await promisify(fs.stat)(path);
|
||||
map.set(path, stat);
|
||||
}
|
||||
return map;
|
||||
|
@ -1,48 +0,0 @@
|
||||
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;
|
||||
export const readdirSync = fs.readdirSync;
|
||||
export const statSync = fs.statSync;
|
||||
export const writeFileSync = fs.writeFileSync;
|
||||
export const readFileSync = fs.readFileSync;
|
||||
export const accessSync = fs.accessSync;
|
||||
export const renameSync = fs.renameSync;
|
||||
|
||||
export const exists = promisify(fs.exists);
|
||||
export const lstat = promisify(fs.lstat);
|
||||
export const readdir = promisify(fs.readdir);
|
||||
export const stat = promisify(fs.stat);
|
||||
export const writeFile = promisify(fs.writeFile);
|
||||
export const readFile = promisify(fs.readFile);
|
||||
export const access = promisify(fs.access);
|
||||
export const rename = promisify(fs.rename);
|
||||
|
||||
export const watchFile = fs.watchFile;
|
||||
export const unwatchFile = fs.unwatchFile;
|
||||
|
||||
export function mkdirp(path: string, timeout: number = 3000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutHandle: NodeJS.Timeout;
|
||||
if (timeout > 0) {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
reject(new Error(`Timeout of ${timeout} ms exceeded while trying to create the directory "${path}"`));
|
||||
}, timeout);
|
||||
}
|
||||
fs.mkdir(path, { recursive: true }, err => {
|
||||
clearTimeout(timeoutHandle);
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function mkdirpSync(path: string): void {
|
||||
fs.mkdirSync(path, { recursive: true });
|
||||
}
|
@ -42,10 +42,6 @@ export class NotificationServiceServerImpl implements NotificationServiceServer
|
||||
this.clients.forEach(client => client.notifyConfigChanged(event));
|
||||
}
|
||||
|
||||
notifySketchbookChanged(event: { created: Sketch[], removed: Sketch[] }): void {
|
||||
this.clients.forEach(client => client.notifySketchbookChanged(event));
|
||||
}
|
||||
|
||||
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
|
||||
this.clients.forEach(client => client.notifyRecentSketchesChanged(event));
|
||||
}
|
||||
|
@ -1,21 +1,19 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as temp from 'temp';
|
||||
import * as path from 'path';
|
||||
import * as nsfw from 'nsfw';
|
||||
import { ncp } from 'ncp';
|
||||
import { Stats } from 'fs';
|
||||
import * as fs from './fs-extra';
|
||||
import { promisify } from 'util';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { FileUri } from '@theia/core/lib/node';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { isWindows } from '@theia/core/lib/common/os';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { SketchesService, Sketch } from '../common/protocol/sketches-service';
|
||||
import { firstToLowerCase } from '../common/utils';
|
||||
import { NotificationServiceServerImpl } from './notification-service-server';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { notEmpty } from '@theia/core';
|
||||
|
||||
// As currently implemented on Linux,
|
||||
// the maximum number of symbolic links that will be followed while resolving a pathname is 40
|
||||
@ -39,78 +37,31 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
async getSketches(uri?: string): Promise<SketchWithDetails[]> {
|
||||
let fsPath: undefined | string;
|
||||
let sketchbookPath: undefined | string;
|
||||
if (!uri) {
|
||||
const { sketchDirUri } = await this.configService.getConfiguration();
|
||||
fsPath = FileUri.fsPath(sketchDirUri);
|
||||
if (!fs.existsSync(fsPath)) {
|
||||
await fs.mkdirp(fsPath);
|
||||
sketchbookPath = FileUri.fsPath(sketchDirUri);
|
||||
if (!await promisify(fs.exists)(sketchbookPath)) {
|
||||
await promisify(fs.mkdir)(sketchbookPath, { recursive: true });
|
||||
}
|
||||
} else {
|
||||
fsPath = FileUri.fsPath(uri);
|
||||
sketchbookPath = FileUri.fsPath(uri);
|
||||
}
|
||||
if (!fs.existsSync(fsPath)) {
|
||||
if (!await promisify(fs.exists)(sketchbookPath)) {
|
||||
return [];
|
||||
}
|
||||
const stat = await fs.stat(fsPath);
|
||||
const stat = await promisify(fs.stat)(sketchbookPath);
|
||||
if (!stat.isDirectory()) {
|
||||
return [];
|
||||
}
|
||||
return this.doGetSketches(fsPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dev note: The keys are filesystem paths, not URI strings.
|
||||
*/
|
||||
private sketchbooks = new Map<string, SketchWithDetails[] | Deferred<SketchWithDetails[]>>();
|
||||
private fireSoonHandle?: NodeJS.Timer;
|
||||
private bufferedSketchbookEvents: { type: 'created' | 'removed', sketch: Sketch }[] = [];
|
||||
|
||||
private fireSoon(type: 'created' | 'removed', sketch: Sketch): void {
|
||||
this.bufferedSketchbookEvents.push({ type, sketch });
|
||||
|
||||
if (this.fireSoonHandle) {
|
||||
clearTimeout(this.fireSoonHandle);
|
||||
}
|
||||
|
||||
this.fireSoonHandle = setTimeout(() => {
|
||||
const event: { created: Sketch[], removed: Sketch[] } = {
|
||||
created: [],
|
||||
removed: []
|
||||
};
|
||||
for (const { type, sketch } of this.bufferedSketchbookEvents) {
|
||||
if (type === 'created') {
|
||||
event.created.push(sketch);
|
||||
} else {
|
||||
event.removed.push(sketch);
|
||||
}
|
||||
}
|
||||
this.notificationService.notifySketchbookChanged(event);
|
||||
this.bufferedSketchbookEvents.length = 0;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes the `fsPath` points to an existing directory.
|
||||
*/
|
||||
private async doGetSketches(sketchbookPath: string): Promise<SketchWithDetails[]> {
|
||||
const resolvedSketches = this.sketchbooks.get(sketchbookPath);
|
||||
if (resolvedSketches) {
|
||||
if (Array.isArray(resolvedSketches)) {
|
||||
return resolvedSketches;
|
||||
}
|
||||
return resolvedSketches.promise;
|
||||
}
|
||||
|
||||
const deferred = new Deferred<SketchWithDetails[]>();
|
||||
this.sketchbooks.set(sketchbookPath, deferred);
|
||||
const sketches: Array<SketchWithDetails> = [];
|
||||
const filenames = await fs.readdir(sketchbookPath);
|
||||
const filenames = await promisify(fs.readdir)(sketchbookPath);
|
||||
for (const fileName of filenames) {
|
||||
const filePath = path.join(sketchbookPath, fileName);
|
||||
if (await this.isSketchFolder(FileUri.create(filePath).toString())) {
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
const stat = await promisify(fs.stat)(filePath);
|
||||
const sketch = await this.loadSketch(FileUri.create(filePath).toString());
|
||||
sketches.push({
|
||||
...sketch,
|
||||
@ -122,88 +73,6 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
}
|
||||
}
|
||||
sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
||||
const deleteSketch = (toDelete: Sketch & { mtimeMs: number }) => {
|
||||
const index = sketches.indexOf(toDelete);
|
||||
if (index !== -1) {
|
||||
console.log(`Sketch '${toDelete.name}' was removed from sketchbook '${sketchbookPath}'.`);
|
||||
sketches.splice(index, 1);
|
||||
sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
||||
this.fireSoon('removed', toDelete);
|
||||
}
|
||||
};
|
||||
const createSketch = async (path: string) => {
|
||||
try {
|
||||
const [stat, sketch] = await Promise.all([
|
||||
fs.stat(path),
|
||||
this.loadSketch(path)
|
||||
]);
|
||||
console.log(`New sketch '${sketch.name}' was crated in sketchbook '${sketchbookPath}'.`);
|
||||
sketches.push({ ...sketch, mtimeMs: stat.mtimeMs });
|
||||
sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
||||
this.fireSoon('created', sketch);
|
||||
} catch { }
|
||||
};
|
||||
const watcher = await nsfw(sketchbookPath, async (events: any) => {
|
||||
// We track `.ino` files changes only.
|
||||
for (const event of events) {
|
||||
switch (event.action) {
|
||||
case nsfw.ActionType.CREATED:
|
||||
if (event.file.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.file === `${path.basename(event.directory)}.ino`) {
|
||||
createSketch(event.directory);
|
||||
}
|
||||
break;
|
||||
case nsfw.ActionType.DELETED:
|
||||
let sketch: Sketch & { mtimeMs: number } | undefined = undefined
|
||||
// Deleting the `ino` file.
|
||||
if (event.file.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.file === `${path.basename(event.directory)}.ino`) {
|
||||
sketch = sketches.find(sketch => FileUri.fsPath(sketch.uri) === event.directory);
|
||||
} else if (event.directory === sketchbookPath) { // Deleting the sketch (or any folder folder in the sketchbook).
|
||||
sketch = sketches.find(sketch => FileUri.fsPath(sketch.uri) === path.join(event.directory, event.file));
|
||||
}
|
||||
if (sketch) {
|
||||
deleteSketch(sketch);
|
||||
}
|
||||
break;
|
||||
case nsfw.ActionType.RENAMED:
|
||||
let sketchToDelete: Sketch & { mtimeMs: number } | undefined = undefined
|
||||
// When renaming with the Java IDE we got an event where `directory` is the sketchbook and `oldFile` is the sketch.
|
||||
if (event.directory === sketchbookPath) {
|
||||
sketchToDelete = sketches.find(sketch => FileUri.fsPath(sketch.uri) === path.join(event.directory, event.oldFile));
|
||||
}
|
||||
|
||||
if (sketchToDelete) {
|
||||
deleteSketch(sketchToDelete);
|
||||
} else {
|
||||
// If it's not a deletion, check for creation. The `directory` is the new sketch and the `newFile` is the new `ino` file.
|
||||
// tslint:disable-next-line:max-line-length
|
||||
if (event.newFile.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.newFile === `${path.basename(event.directory)}.ino`) {
|
||||
createSketch(event.directory);
|
||||
} else {
|
||||
// When renaming the `ino` file directly on the filesystem. The `directory` is the sketch and `newFile` and `oldFile` is the `ino` file.
|
||||
// tslint:disable-next-line:max-line-length
|
||||
if (event.oldFile.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.oldFile === `${path.basename(event.directory)}.ino`) {
|
||||
sketchToDelete = sketches.find(sketch => FileUri.fsPath(sketch.uri) === event.directory, event.oldFile);
|
||||
}
|
||||
if (sketchToDelete) {
|
||||
deleteSketch(sketchToDelete);
|
||||
} else if (event.directory === sketchbookPath) {
|
||||
createSketch(path.join(event.directory, event.newFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: no `await` for some reason this blocks the workspace root initialization on Windows inside a bundled electron app.
|
||||
console.log(`Starting to watch sketchbook at '${sketchbookPath}'.`);
|
||||
watcher.start()
|
||||
.then(() => console.log(`Initialized watcher in sketchbook: '${sketchbookPath}. Watching for sketch changes.`))
|
||||
.catch(err => console.error(`Failed to initialize watcher in sketchbook '${sketchbookPath}'. Cannot track sketch changes.`, err));
|
||||
|
||||
deferred.resolve(sketches);
|
||||
this.sketchbooks.set(sketchbookPath, sketches);
|
||||
return sketches;
|
||||
}
|
||||
|
||||
@ -214,11 +83,11 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
*/
|
||||
async loadSketch(uri: string): Promise<SketchWithDetails> {
|
||||
const sketchPath = FileUri.fsPath(uri);
|
||||
const exists = await fs.exists(sketchPath);
|
||||
const exists = await promisify(fs.exists)(sketchPath);
|
||||
if (!exists) {
|
||||
throw new Error(`${uri} does not exist.`);
|
||||
}
|
||||
const stat = await fs.lstat(sketchPath);
|
||||
const stat = await promisify(fs.lstat)(sketchPath);
|
||||
let sketchFolder: string | undefined;
|
||||
let mainSketchFile: string | undefined;
|
||||
|
||||
@ -228,7 +97,7 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
// Allowed extensions are .ino and .pde (but not both)
|
||||
for (const extension of Sketch.Extensions.MAIN) {
|
||||
const candidateSketchFile = path.join(sketchPath, `${path.basename(sketchPath)}${extension}`);
|
||||
const candidateExists = await fs.exists(candidateSketchFile);
|
||||
const candidateExists = await promisify(fs.exists)(candidateSketchFile);
|
||||
if (candidateExists) {
|
||||
if (!mainSketchFile) {
|
||||
mainSketchFile = candidateSketchFile;
|
||||
@ -245,12 +114,12 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
|
||||
// Check main file is readable.
|
||||
try {
|
||||
await fs.access(mainSketchFile, fs.constants.R_OK);
|
||||
await promisify(fs.access)(mainSketchFile, fs.constants.R_OK);
|
||||
} catch {
|
||||
throw new Error('Unable to open the main sketch file.');
|
||||
}
|
||||
|
||||
const mainSketchFileStat = await fs.lstat(mainSketchFile);
|
||||
const mainSketchFileStat = await promisify(fs.lstat)(mainSketchFile);
|
||||
if (mainSketchFileStat.isDirectory()) {
|
||||
throw new Error(`Sketch must not be a directory.`);
|
||||
}
|
||||
@ -289,7 +158,7 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(fsPath, fs.constants.R_OK);
|
||||
await promisify(fs.access)(fsPath, fs.constants.R_OK);
|
||||
files.push(fsPath);
|
||||
} catch { }
|
||||
|
||||
@ -312,7 +181,7 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
private async loadRecentSketches(fsPath: string): Promise<Record<string, number>> {
|
||||
let data: Record<string, number> = {};
|
||||
try {
|
||||
const raw = await fs.readFile(fsPath, { encoding: 'utf8' });
|
||||
const raw = await promisify(fs.readFile)(fsPath, { encoding: 'utf8' });
|
||||
data = JSON.parse(raw);
|
||||
} catch { }
|
||||
return data;
|
||||
@ -349,7 +218,7 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
delete data[toDeleteUri];
|
||||
}
|
||||
|
||||
await fs.writeFile(fsPath, JSON.stringify(data, null, 2));
|
||||
await promisify(fs.writeFile)(fsPath, JSON.stringify(data, null, 2));
|
||||
this.recentlyOpenedSketches().then(sketches => this.notificationService.notifyRecentSketchesChanged({ sketches }));
|
||||
}
|
||||
|
||||
@ -358,23 +227,18 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
const fsPath = path.join(FileUri.fsPath(configDirUri), 'recent-sketches.json');
|
||||
let data: Record<string, number> = {};
|
||||
try {
|
||||
const raw = await fs.readFile(fsPath, { encoding: 'utf8' });
|
||||
const raw = await promisify(fs.readFile)(fsPath, { encoding: 'utf8' });
|
||||
data = JSON.parse(raw);
|
||||
} catch { }
|
||||
|
||||
const loadSketchSafe = (uri: string) => {
|
||||
const sketches: SketchWithDetails[] = []
|
||||
for (const uri of Object.keys(data).sort((left, right) => data[right] - data[left])) {
|
||||
try {
|
||||
return this.loadSketch(uri);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const sketch = await this.loadSketch(uri);
|
||||
sketches.push(sketch);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
const sketches = await Promise.all(Object.keys(data)
|
||||
.sort((left, right) => data[right] - data[left])
|
||||
.map(loadSketchSafe)
|
||||
.filter(notEmpty));
|
||||
|
||||
return sketches;
|
||||
}
|
||||
|
||||
@ -410,7 +274,7 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
additionalFiles.sort();
|
||||
otherSketchFiles.sort();
|
||||
|
||||
const { mtimeMs } = await fs.lstat(sketchFolderPath);
|
||||
const { mtimeMs } = await promisify(fs.lstat)(sketchFolderPath);
|
||||
return {
|
||||
uri: FileUri.create(sketchFolderPath).toString(),
|
||||
mainFileUri: FileUri.create(mainFile).toString(),
|
||||
@ -461,7 +325,7 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
maxDepth--;
|
||||
const files: string[] = [];
|
||||
try {
|
||||
files.push(...await fs.readdir(root));
|
||||
files.push(...await promisify(fs.readdir)(root));
|
||||
} catch { }
|
||||
for (const file of files) {
|
||||
err = await this.simpleLocalWalk(path.join(root, file), maxDepth, walk);
|
||||
@ -475,12 +339,12 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
}
|
||||
|
||||
private async lstat(fsPath: string): Promise<{ info: Stats, err: undefined } | { info: undefined, err: Error }> {
|
||||
const exists = await fs.exists(fsPath);
|
||||
const exists = await promisify(fs.exists)(fsPath);
|
||||
if (!exists) {
|
||||
return { info: undefined, err: new Error(`${fsPath} does not exist`) };
|
||||
}
|
||||
try {
|
||||
const info = await fs.lstat(fsPath);
|
||||
const info = await promisify(fs.lstat)(fsPath);
|
||||
return { info, err: undefined };
|
||||
} catch (err) {
|
||||
return { info: undefined, err };
|
||||
@ -506,7 +370,7 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
for (let i = 97; i < 97 + 26; i++) {
|
||||
let sketchNameCandidate = `${sketchBaseName}${String.fromCharCode(i)}`;
|
||||
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
|
||||
if (fs.existsSync(path.join(user, sketchNameCandidate))) {
|
||||
if (await promisify(fs.exists)(path.join(user, sketchNameCandidate))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -520,8 +384,8 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
|
||||
const sketchDir = path.join(parentPath, sketchName)
|
||||
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
|
||||
await fs.mkdirp(sketchDir);
|
||||
await fs.writeFile(sketchFile, `void setup() {
|
||||
await promisify(fs.mkdir)(sketchDir, { recursive: true });
|
||||
await promisify(fs.writeFile)(sketchFile, `void setup() {
|
||||
// put your setup code here, to run once:
|
||||
|
||||
}
|
||||
@ -550,9 +414,13 @@ void loop() {
|
||||
|
||||
async isSketchFolder(uri: string): Promise<boolean> {
|
||||
const fsPath = FileUri.fsPath(uri);
|
||||
if (fs.existsSync(fsPath) && fs.lstatSync(fsPath).isDirectory()) {
|
||||
let stat: fs.Stats | undefined;
|
||||
try {
|
||||
stat = await promisify(fs.lstat)(fsPath);
|
||||
} catch { }
|
||||
if (stat && stat.isDirectory()) {
|
||||
const basename = path.basename(fsPath);
|
||||
const files = await fs.readdir(fsPath);
|
||||
const files = await promisify(fs.readdir)(fsPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (files[i] === basename + '.ino') {
|
||||
try {
|
||||
@ -583,7 +451,7 @@ void loop() {
|
||||
|
||||
async copy(sketch: Sketch, { destinationUri }: { destinationUri: string }): Promise<string> {
|
||||
const source = FileUri.fsPath(sketch.uri);
|
||||
const exists = await fs.exists(source);
|
||||
const exists = await promisify(fs.exists)(source);
|
||||
if (!exists) {
|
||||
throw new Error(`Sketch does not exist: ${sketch}`);
|
||||
}
|
||||
@ -604,7 +472,7 @@ void loop() {
|
||||
const oldPath = path.join(destination, new URI(sketch.mainFileUri).path.base);
|
||||
const newPath = path.join(destination, `${newName}.ino`);
|
||||
if (oldPath !== newPath) {
|
||||
await fs.rename(oldPath, newPath);
|
||||
await promisify(fs.rename)(oldPath, newPath);
|
||||
}
|
||||
await this.loadSketch(destinationUri); // Sanity check.
|
||||
resolve();
|
||||
|
@ -12,7 +12,7 @@ export class HostedPluginReader extends TheiaHostedPluginReader {
|
||||
protected cliConfigSchemaUri: string;
|
||||
|
||||
async onStart(): Promise<void> {
|
||||
this.cliConfigSchemaUri = await this.configService.getConfigurationFileSchemaUri();
|
||||
this.cliConfigSchemaUri = ''; // TODO: this was removed in another PR.
|
||||
}
|
||||
|
||||
readContribution(plugin: PluginPackage): PluginContribution | undefined {
|
||||
|
@ -1,78 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import { safeLoad } from 'js-yaml';
|
||||
import { ArduinoDaemonImpl } from '../../node/arduino-daemon-impl';
|
||||
import { ConfigFileValidator } from '../../node/config-file-validator';
|
||||
import { spawnCommand } from '../../node/exec-util';
|
||||
|
||||
class MockConfigFileValidator extends ConfigFileValidator {
|
||||
|
||||
protected async isValidPath(path: string): Promise<boolean> {
|
||||
if (path.endsWith('!invalid')) {
|
||||
return false;
|
||||
}
|
||||
return super.isValidPath(path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
describe('config-file-validator', () => {
|
||||
|
||||
const testMe = new MockConfigFileValidator();
|
||||
|
||||
it('valid - default', async () => {
|
||||
const config = await defaultConfig();
|
||||
const result = await testMe.validate(config);
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("valid - no 'board_manager'", async () => {
|
||||
const config = await defaultConfig();
|
||||
delete config.board_manager;
|
||||
const result = await testMe.validate(config);
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("valid - no 'board_manager.additional_urls'", async () => {
|
||||
const config = await defaultConfig();
|
||||
delete config.board_manager.additional_urls;
|
||||
const result = await testMe.validate(config);
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("invalid - no 'directories.data'", async () => {
|
||||
const config = await defaultConfig();
|
||||
delete config.directories.data;
|
||||
const result = await testMe.validate(config);
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("invalid - 'directories.data' is a empty string", async () => {
|
||||
const config = await defaultConfig();
|
||||
config.directories.data = '';
|
||||
const result = await testMe.validate(config);
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("invalid - 'directories.data' is contains invalid chars", async () => {
|
||||
const config = await defaultConfig();
|
||||
config.directories.data = '!invalid';
|
||||
const result = await testMe.validate(config);
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
async function defaultConfig(): Promise<any> {
|
||||
return new Promise<any>(resolve => {
|
||||
new ArduinoDaemonImpl().getExecPath()
|
||||
.then(execPath => spawnCommand(execPath, ['config', 'dump']))
|
||||
.then(content => safeLoad(content))
|
||||
.then(config => resolve(config));
|
||||
});
|
||||
}
|
||||
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user