diff --git a/.gitignore b/.gitignore index ddaa0df3..f9597d13 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ build/ downloads/ !electron/build/ src-gen/ -arduino-ide-*/webpack.config.js +browser-app/webpack.config.js +electron-app/webpack.config.js .DS_Store /workspace/static # switching from `electron` to `browser` in dev mode. diff --git a/.gitpod.yml b/.gitpod.yml index 5205cf06..a74a845c 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -8,7 +8,7 @@ ports: tasks: - init: > yarn && - yarn --cwd ./arduino-ide-browser start + yarn --cwd ./browser-app start github: prebuilds: diff --git a/.vscode/launch.json b/.vscode/launch.json index da9bf2ac..c2791d75 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "type": "node", "request": "launch", "name": "Launch Backend", - "program": "${workspaceRoot}/arduino-ide-browser/src-gen/backend/main.js", + "program": "${workspaceRoot}/browser-app/src-gen/backend/main.js", "args": [ "--hostname=0.0.0.0", "--port=3000", @@ -26,8 +26,8 @@ }, "sourceMaps": true, "outFiles": [ - "${workspaceRoot}/arduino-ide-browser/src-gen/backend/*.js", - "${workspaceRoot}/arduino-ide-browser/lib/**/*.js", + "${workspaceRoot}/browser-app/src-gen/backend/*.js", + "${workspaceRoot}/browser-app/lib/**/*.js", "${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js" ], "smartStep": true, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9ce4065b..80a54749 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,9 +4,9 @@ "version": "2.0.0", "tasks": [ { - "label": "Arduino-PoC - Start Browser Example", + "label": "Arduino Editor - Start Browser Example", "type": "shell", - "command": "yarn --cwd ./arduino-ide-browser start", + "command": "yarn --cwd ./browser-app start", "group": "build", "presentation": { "reveal": "always", @@ -15,7 +15,7 @@ } }, { - "label": "Arduino-PoC - Watch Theia Extension", + "label": "Arduino Editor - Watch Theia Extension", "type": "shell", "command": "yarn --cwd ./arduino-ide-extension watch", "group": "build", @@ -26,9 +26,9 @@ } }, { - "label": "Arduino-PoC - Watch Browser Example", + "label": "Arduino Editor - Watch Browser Example", "type": "shell", - "command": "yarn --cwd ./arduino-ide-browser watch", + "command": "yarn --cwd ./browser-app watch", "group": "build", "presentation": { "reveal": "always", @@ -37,11 +37,11 @@ } }, { - "label": "Arduino-PoC - Watch All", + "label": "Arduino Editor - Watch All", "type": "shell", "dependsOn": [ - "Arduino-PoC - Watch Theia Extension", - "Arduino-PoC - Watch Browser Example" + "Arduino Editor - Watch Theia Extension", + "Arduino Editor - Watch Browser Example" ] } ] diff --git a/README.md b/README.md index 30c85685..3af44b6f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ git clone https://github.com/bcmi-labs/arduino-editor cd arduino-editor yarn yarn rebuild:electron -yarn --cwd arduino-ide-electron start +yarn --cwd electron-app start ``` If you want to switch back to the browser-based example, execute the following in the repository root @@ -26,7 +26,7 @@ yarn rebuild:browser ``` Then you can start the browser example again: ``` -yarn --cwd arduino-ide-browser start +yarn --cwd browser-app start ``` ## Arduino-PoC Electron Application diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 537c4b45..861026a0 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -12,6 +12,7 @@ "@theia/core": "next", "@theia/editor": "next", "@theia/filesystem": "next", + "@theia/git": "next", "@theia/languages": "next", "@theia/markers": "next", "@theia/monaco": "next", @@ -19,7 +20,6 @@ "@theia/workspace": "next", "@theia/navigator": "next", "@theia/terminal": "next", - "@theia/git": "next", "@theia/search-in-workspace": "next", "@types/ps-tree": "^1.1.0", "@types/which": "^1.3.1", @@ -40,11 +40,12 @@ }, "devDependencies": { "decompress": "^4.2.0", - "decompress-tarbz2": "^4.1.1", + "decompress-targz": "^4.1.1", "decompress-unzip": "^4.0.1", "download": "^7.1.0", "grpc-tools": "^1.7.3", "grpc_tools_node_protoc_ts": "^2.5.0", + "moment": "^2.24.0", "ncp": "^2.0.0", "rimraf": "^2.6.1", "shelljs": "^0.8.3", diff --git a/arduino-ide-extension/scripts/download-cli.js b/arduino-ide-extension/scripts/download-cli.js index 27bd76e5..62506946 100755 --- a/arduino-ide-extension/scripts/download-cli.js +++ b/arduino-ide-extension/scripts/download-cli.js @@ -1,11 +1,16 @@ // @ts-check -// The links to the downloads as of today (11.08.) are the followings: -// - https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli-nightly-latest-${FILE_NAME} -// - https://downloads.arduino.cc/arduino-cli/arduino-cli-latest-${FILE_NAME} +// The links to the downloads as of today (02.09.) are the followings: +// In order to get the latest nightly build for your platform use the following links replacing with the current date, using the format YYYYMMDD (i.e for 2019/Aug/06 use 20190806 ) +// Linux 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-_Linux_64bit.tar.gz +// Linux ARM 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-_Linux_ARM64.tar.gz +// Windows 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-_Windows_64bit.zip +// Mac OSX: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-_macOS_64bit.tar.gz +// [...] +// redirecting to latest generated builds by replacing latest with the latest available build date, using the format YYYYMMDD (i.e for 2019/Aug/06 latest is replaced with 20190806 (async () => { - const DEFAULT_VERSION = 'nightly'; + const DEFAULT_VERSION = 'latest'; // require('moment')().format('YYYYMMDD'); const os = require('os'); const fs = require('fs'); @@ -14,7 +19,7 @@ const download = require('download'); const decompress = require('decompress'); const unzip = require('decompress-unzip'); - const untarbz = require('decompress-tarbz2'); + const untargz = require('decompress-targz'); process.on('unhandledRejection', (reason, _) => { shell.echo(String(reason)); @@ -31,11 +36,7 @@ .option('cli-version', { alias: 'cv', default: DEFAULT_VERSION, - choices: [ - // 'latest', // TODO: How do we get the source for `latest`. Currently, `latest` is the `0.3.7-alpha.preview`. - 'nightly' - ], - describe: `The version of the 'arduino-cli' to download. Defaults to ${DEFAULT_VERSION}.` + describe: `The version of the 'arduino-cli' to download with the YYYYMMDD format, or 'latest'. Defaults to ${DEFAULT_VERSION}.` }) .option('force-download', { alias: 'fd', @@ -68,13 +69,12 @@ const suffix = (() => { switch (platform) { - case 'darwin': return 'macosx.zip'; - case 'win32': return 'windows.zip'; + case 'darwin': return 'macOS_64bit.tar.gz'; + case 'win32': return 'Windows_64bit.zip'; case 'linux': { switch (arch) { - case 'arm64': return 'linuxarm.tar.bz2'; - case 'x32': return 'linux32.tar.bz2'; - case 'x64': return 'linux64.tar.bz2'; + case 'arm64': return 'Linux_ARM64.tar.gz'; + case 'x64': return 'Linux_64bit.tar.gz'; default: return undefined; } } @@ -86,7 +86,7 @@ shell.exit(1); } - const url = `https://downloads.arduino.cc/arduino-cli/${version === 'nightly' ? 'nightly/' : ''}arduino-cli-${version}-latest-${suffix}`; + const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`; shell.echo(`>>> Downloading 'arduino-cli' from '${url}'...`); const data = await download(url); shell.echo(`<<< Download succeeded.`); @@ -94,16 +94,21 @@ const files = await decompress(data, downloads, { plugins: [ unzip(), - untarbz() + untargz() ] }); - shell.echo('<<< Decompressing succeeded.'); - - if (files.length !== 1) { + if (files.length === 0) { shell.echo('Error ocurred when decompressing the CLI.'); shell.exit(1); } - if (shell.mv('-f', path.join(downloads, files[0].path), cli).code !== 0) { + const cliIndex = files.findIndex(f => f.path.startsWith('arduino-cli')); + if (cliIndex === -1) { + shell.echo('The downloaded artifact does not contains the CLI.'); + shell.exit(1); + } + shell.echo('<<< Decompressing succeeded.'); + + if (shell.mv('-f', path.join(downloads, files[cliIndex].path), cli).code !== 0) { shell.echo(`Could not move file to ${cli}.`); shell.exit(1); } diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index e62c192e..9a778948 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -46,6 +46,7 @@ import { BoardsConfigDialog } from './boards/boards-config-dialog'; import { BoardsToolBarItem } from './boards/boards-toolbar-item'; import { BoardsConfig } from './boards/boards-config'; import { MonitorService } from '../common/protocol/monitor-service'; +import { ConfigService } from '../common/protocol/config-service'; export namespace ArduinoMenus { export const SKETCH = [...MAIN_MENU_BAR, '3_sketch']; @@ -133,21 +134,19 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C @inject(LabelProvider) protected readonly labelProvider: LabelProvider; - + @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService; + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + @inject(ConfigService) + protected readonly configService: ConfigService; + protected boardsToolbarItem: BoardsToolBarItem | null; protected wsSketchCount: number = 0; - constructor(@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService) { - this.workspaceService.onWorkspaceChanged(() => { - if (this.workspaceService.workspace) { - this.registerSketchesInMenu(this.menuRegistry); - } - }) - } - @postConstruct() protected async init(): Promise { // This is a hack. Otherwise, the backend services won't bind. @@ -161,6 +160,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C } this.boardsServiceClient.onBoardsConfigChanged(updateStatusBar); updateStatusBar(this.boardsServiceClient.boardsConfig); + + this.registerSketchesInMenu(this.menuRegistry); } registerToolbarItems(registry: TabBarToolbarRegistry): void { @@ -453,7 +454,13 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C } protected async getWorkspaceSketches(): Promise { - const sketches = this.sketches.getSketches(this.workspaceService.workspace); + + let sketches: Sketch[] = []; + const config = await this.configService.getConfiguration(); + const stat = await this.fileSystem.getFileStat(config.sketchDirUri); + if (!!stat) { + sketches = await this.sketches.getSketches(stat); + } return sketches; } @@ -461,13 +468,18 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C const url = new URL(window.location.href); const currentSketch = url.searchParams.get('sketch'); // Nothing to do if we want to open the same sketch which is already opened. - if (!!currentSketch && new URI(currentSketch).toString() === new URI(uri).toString()) { - this.messageService.info(`The '${this.labelProvider.getLongName(new URI(uri))}' is already opened.`); + const sketchUri = new URI(uri); + if (!!currentSketch && new URI(currentSketch).toString() === sketchUri.toString()) { + this.messageService.info(`The '${this.labelProvider.getLongName(sketchUri)}' is already opened.`); // NOOP. return; } // Preserve the current window if the `sketch` is not in the `searchParams`. url.searchParams.set('sketch', uri); + const hash = await this.fileSystem.getFsPath(sketchUri.toString()); + if (hash) { + url.hash = hash; + } if (!currentSketch) { setTimeout(() => window.location.href = url.toString(), 100); return; @@ -543,25 +555,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C return undefined; } - // private async onNoBoardsInstalled() { - // const action = await this.messageService.info("You have no boards installed. Use the boards manager to install one.", "Open Boards Manager"); - // if (!action) { - // return; - // } - - // this.boardsListWidgetFrontendContribution.openView({ reveal: true }); - // } - - // private async onUnknownBoard() { - // const action = await this.messageService.warn("There's a board connected for which you need to install software." + - // " If this were not a PoC we would offer you the right package now.", "Open Boards Manager"); - // if (!action) { - // return; - // } - - // this.boardsListWidgetFrontendContribution.openView({ reveal: true }); - // } - private isArduinoToolbar(maybeToolbarWidget: any): boolean { if (maybeToolbarWidget instanceof ArduinoToolbar) { return true; diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 2cd6b476..4e74e2c9 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -56,6 +56,7 @@ import { LibraryItemRenderer } from './library/library-item-renderer'; import { BoardItemRenderer } from './boards/boards-item-renderer'; import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl'; import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service'; +import { ConfigService, ConfigServicePath } from '../common/protocol/config-service'; const ElementQueries = require('css-element-queries/src/ElementQueries'); if (!ARDUINO_PRO_MODE) { @@ -96,6 +97,9 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un // Sketch list service bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope(); + // Config service + bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope(); + // Boards service bind(BoardsService).toDynamicValue(context => { const connection = context.container.get(WebSocketConnectionProvider); diff --git a/arduino-ide-extension/src/browser/arduino-workspace-service.ts b/arduino-ide-extension/src/browser/arduino-workspace-service.ts index 304a33e8..e47c6899 100644 --- a/arduino-ide-extension/src/browser/arduino-workspace-service.ts +++ b/arduino-ide-extension/src/browser/arduino-workspace-service.ts @@ -4,6 +4,7 @@ import { WorkspaceServer } from "@theia/workspace/lib/common"; import { FileSystem, FileStat } from "@theia/filesystem/lib/common"; import URI from "@theia/core/lib/common/uri"; import { SketchFactory } from "./sketch-factory"; +import { ConfigService } from "../common/protocol/config-service"; /** * This is workaround to have custom frontend binding for the default workspace, although we @@ -21,16 +22,14 @@ export class AWorkspaceService extends WorkspaceService { @inject(SketchFactory) protected readonly sketchFactory: SketchFactory; + @inject(ConfigService) + protected readonly configService: ConfigService; + protected async getDefaultWorkspacePath(): Promise { let result = await super.getDefaultWorkspacePath(); if (!result) { - const userHome = await this.fileSystem.getCurrentUserHome(); - if (!userHome) { - return; - } - - // The backend has created this location if it was missing. - result = new URI(userHome.uri).resolve('Arduino-PoC').resolve('Sketches').toString(); + const config = await this.configService.getConfiguration(); + result = config.sketchDirUri; } const stat = await this.fileSystem.getFileStat(result); diff --git a/arduino-ide-extension/src/browser/sketch-factory.ts b/arduino-ide-extension/src/browser/sketch-factory.ts index efb70981..2a50aa61 100644 --- a/arduino-ide-extension/src/browser/sketch-factory.ts +++ b/arduino-ide-extension/src/browser/sketch-factory.ts @@ -38,7 +38,8 @@ export class SketchFactory { const sketchDir = parent.resolve(sketchName); const sketchFile = sketchDir.resolve(`${sketchName}.ino`); this.fileSystem.createFolder(sketchDir.toString()); - this.fileSystem.createFile(sketchFile.toString(), { content: ` + this.fileSystem.createFile(sketchFile.toString(), { + content: ` void setup() { // put your setup code here, to run once: @@ -50,7 +51,11 @@ void loop() { } ` }); const location = new URL(window.location.href); - location.searchParams.set('sketch', sketchFile.toString()); + location.searchParams.set('sketch', sketchDir.toString()); + const hash = await this.fileSystem.getFsPath(sketchDir.toString()); + if (hash) { + location.hash = hash; + } this.windowService.openNewWindow(location.toString()); } catch (e) { throw new Error("Cannot create new sketch: " + e); diff --git a/arduino-ide-extension/src/common/protocol/config-service.ts b/arduino-ide-extension/src/common/protocol/config-service.ts new file mode 100644 index 00000000..fb8c90cd --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/config-service.ts @@ -0,0 +1,11 @@ +export const ConfigServicePath = '/services/config-service'; +export const ConfigService = Symbol('ConfigService'); + +export interface ConfigService { + getConfiguration(): Promise; +} + +export interface Config { + sketchDirUri: string; + dataDirUri: string; +} diff --git a/arduino-ide-extension/src/node/arduino-backend-module.ts b/arduino-ide-extension/src/node/arduino-backend-module.ts index 3d50f866..daeb3878 100644 --- a/arduino-ide-extension/src/node/arduino-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-backend-module.ts @@ -19,11 +19,24 @@ import { DefaultWorkspaceServerExt } from './default-workspace-server-ext'; import { WorkspaceServer } from '@theia/workspace/lib/common'; import { SketchesServiceImpl } from './sketches-service-impl'; import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service'; +import { ConfigService, ConfigServicePath } from '../common/protocol/config-service'; import { MonitorServiceImpl } from './monitor/monitor-service-impl'; import { MonitorService, MonitorServicePath, MonitorServiceClient } from '../common/protocol/monitor-service'; import { MonitorClientProvider } from './monitor/monitor-client-provider'; +import { ArduinoCli } from './arduino-cli'; +import { ArduinoCliContribution } from './arduino-cli-contribution'; +import { CliContribution } from '@theia/core/lib/node'; +import { ConfigServiceImpl } from './config-service-impl'; export default new ContainerModule((bind, unbind, isBound, rebind) => { + // Theia backend CLI contribution. + bind(ArduinoCliContribution).toSelf().inSingletonScope(); + bind(CliContribution).toService(ArduinoCliContribution); + + // Provides the path of the Ardunio CLI. + bind(ArduinoCli).toSelf().inSingletonScope(); + + // Shared daemonn bind(ArduinoDaemon).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(ArduinoDaemon); @@ -42,6 +55,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bindBackendService(SketchesServicePath, SketchesService); }); bind(ConnectionContainerModule).toConstantValue(sketchesServiceConnectionModule); + + bind(ConfigServiceImpl).toSelf().inSingletonScope(); + bind(ConfigService).toService(ConfigServiceImpl); + + // Config service + const configServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { + bindBackendService(ConfigServicePath, ConfigService); + }); + bind(ConnectionContainerModule).toConstantValue(configServiceConnectionModule); // Boards service const boardsServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { diff --git a/arduino-ide-extension/src/node/arduino-cli-contribution.ts b/arduino-ide-extension/src/node/arduino-cli-contribution.ts new file mode 100644 index 00000000..148b92b5 --- /dev/null +++ b/arduino-ide-extension/src/node/arduino-cli-contribution.ts @@ -0,0 +1,27 @@ +import { injectable } from 'inversify'; +import { Argv, Arguments } from 'yargs'; +import { CliContribution } from '@theia/core/lib/node'; + +@injectable() +export class ArduinoCliContribution implements CliContribution { + + protected _debugCli = false + + configure(conf: Argv): void { + conf.option('debug-cli', { + description: 'Can be specified if the CLI daemon process was started externally.', + type: 'boolean', + default: false, + nargs: 1 + }); + } + + setArguments(args: Arguments): void { + this._debugCli = args['debug-cli']; + } + + get debugCli(): boolean { + return this._debugCli; + } + +} diff --git a/arduino-ide-extension/src/node/arduino-cli.ts b/arduino-ide-extension/src/node/arduino-cli.ts new file mode 100644 index 00000000..5168c8cc --- /dev/null +++ b/arduino-ide-extension/src/node/arduino-cli.ts @@ -0,0 +1,66 @@ +import * as os from 'os'; +import * as which from 'which'; +import * as cp from 'child_process'; +import { join, delimiter } from 'path'; +import { injectable, inject } from 'inversify'; +import { ILogger } from '@theia/core'; +import { FileUri } from '@theia/core/lib/node/file-uri'; +import { Config } from '../common/protocol/config-service'; + +@injectable() +export class ArduinoCli { + + @inject(ILogger) + protected logger: ILogger; + + async getExecPath(): Promise { + const build = join(__dirname, '..', '..', 'build'); + return new Promise((resolve, reject) => { + which(`arduino-cli${os.platform() === 'win32' ? '.exe' : ''}`, { path: `${process.env.PATH}${delimiter}${build}` }, (err, path) => { + if (err) { + reject(err); + return; + } + resolve(path); + }); + }); + } + + async getDefaultConfig(): Promise { + const command = await this.getExecPath(); + return new Promise((resolve, reject) => { + cp.execFile( + command, + ['config', 'dump', '--format', 'json'], + { encoding: 'utf8' }, + (error, stdout, stderr) => { + + if (error) { + throw error; + } + + if (stderr) { + throw new Error(stderr); + } + + const { sketchbook_path, arduino_data } = JSON.parse(stdout.trim()); + + if (!sketchbook_path) { + reject(new Error(`Could not parse config. 'sketchbook_path' was missing from: ${stdout}`)); + return; + } + + if (!arduino_data) { + reject(new Error(`Could not parse config. 'arduino_data' was missing from: ${stdout}`)); + return; + } + + resolve({ + sketchDirUri: FileUri.create(sketchbook_path).toString(), + dataDirUri: FileUri.create(arduino_data).toString() + }); + }); + }); + } + +} diff --git a/arduino-ide-extension/src/node/arduino-daemon.ts b/arduino-ide-extension/src/node/arduino-daemon.ts index 767a3452..b9ee058a 100644 --- a/arduino-ide-extension/src/node/arduino-daemon.ts +++ b/arduino-ide-extension/src/node/arduino-daemon.ts @@ -1,6 +1,4 @@ -import * as which from 'which'; -import * as os from 'os'; -import { join, delimiter } from 'path'; +import { join } from 'path'; import { exec, spawn, SpawnOptions } from 'child_process'; import { inject, injectable, named } from 'inversify'; import { ILogger } from '@theia/core/lib/common/logger'; @@ -9,17 +7,22 @@ import { Deferred } from '@theia/core/lib/common/promise-util'; import { environment } from '@theia/application-package/lib/environment'; import { DaemonLog } from './daemon-log'; import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; +import { ArduinoCliContribution } from './arduino-cli-contribution'; +import { ArduinoCli } from './arduino-cli'; @injectable() export class ArduinoDaemon implements BackendApplicationContribution { - // Set this to `true` if you want to connect to a CLI running in debug mode. - static DEBUG_CLI = false; - @inject(ILogger) @named('daemon') protected readonly logger: ILogger + @inject(ArduinoCli) + protected readonly cli: ArduinoCli; + + @inject(ArduinoCliContribution) + protected readonly cliContribution: ArduinoCliContribution; + @inject(ToolOutputServiceServer) protected readonly toolOutputService: ToolOutputServiceServer; @@ -27,19 +30,10 @@ export class ArduinoDaemon implements BackendApplicationContribution { async onStart() { try { - if (!ArduinoDaemon.DEBUG_CLI) { - const build = join(__dirname, '..', '..', 'build'); - const executable = await new Promise((resolve, reject) => { - which(`arduino-cli${os.platform() === 'win32' ? '.exe' : ''}`, { path: `${process.env.PATH}${delimiter}${build}` }, (err, path) => { - if (err) { - reject(err); - return; - } - resolve(path); - }); - }); + if (!this.cliContribution.debugCli) { + const executable = await this.cli.getExecPath(); this.logger.info(`>>> Starting 'arduino-cli' daemon... [${executable}]`); - const daemon = exec(`${executable} --debug daemon`, (err, stdout, stderr) => { + const daemon = exec(`${executable} daemon -v --log-level info --format json --log-format json`, (err, stdout, stderr) => { if (err || stderr) { console.log(err || new Error(stderr)); return; @@ -74,7 +68,7 @@ export class ArduinoDaemon implements BackendApplicationContribution { await new Promise(resolve => setTimeout(resolve, 2000)); this.isReady.resolve(); - if (!ArduinoDaemon.DEBUG_CLI) { + if (!this.cliContribution.debugCli) { this.logger.info(`<<< The 'arduino-cli' daemon is up an running.`); } else { this.logger.info(`Assuming the 'arduino-cli' already runs in debug mode.`); diff --git a/arduino-ide-extension/src/node/cli-protocol/commands/compile_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/commands/compile_pb.d.ts index e27969ce..39959cd0 100644 --- a/arduino-ide-extension/src/node/cli-protocol/commands/compile_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/commands/compile_pb.d.ts @@ -51,6 +51,9 @@ export class CompileReq extends jspb.Message { getExportfile(): string; setExportfile(value: string): void; + getJobs(): number; + setJobs(value: number): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): CompileReq.AsObject; @@ -77,6 +80,7 @@ export namespace CompileReq { quiet: boolean, vidpid: string, exportfile: string, + jobs: number, } } diff --git a/arduino-ide-extension/src/node/cli-protocol/commands/compile_pb.js b/arduino-ide-extension/src/node/cli-protocol/commands/compile_pb.js index 338f9d8e..1b3f213f 100644 --- a/arduino-ide-extension/src/node/cli-protocol/commands/compile_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/commands/compile_pb.js @@ -81,7 +81,8 @@ proto.cc.arduino.cli.commands.CompileReq.toObject = function(includeInstance, ms verbose: jspb.Message.getFieldWithDefault(msg, 10, false), quiet: jspb.Message.getFieldWithDefault(msg, 11, false), vidpid: jspb.Message.getFieldWithDefault(msg, 12, ""), - exportfile: jspb.Message.getFieldWithDefault(msg, 13, "") + exportfile: jspb.Message.getFieldWithDefault(msg, 13, ""), + jobs: jspb.Message.getFieldWithDefault(msg, 14, 0) }; if (includeInstance) { @@ -171,6 +172,10 @@ proto.cc.arduino.cli.commands.CompileReq.deserializeBinaryFromReader = function( var value = /** @type {string} */ (reader.readString()); msg.setExportfile(value); break; + case 14: + var value = /** @type {number} */ (reader.readInt32()); + msg.setJobs(value); + break; default: reader.skipField(); break; @@ -292,6 +297,13 @@ proto.cc.arduino.cli.commands.CompileReq.serializeBinaryToWriter = function(mess f ); } + f = message.getJobs(); + if (f !== 0) { + writer.writeInt32( + 14, + f + ); + } }; @@ -527,6 +539,21 @@ proto.cc.arduino.cli.commands.CompileReq.prototype.setExportfile = function(valu }; +/** + * optional int32 jobs = 14; + * @return {number} + */ +proto.cc.arduino.cli.commands.CompileReq.prototype.getJobs = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 14, 0)); +}; + + +/** @param {number} value */ +proto.cc.arduino.cli.commands.CompileReq.prototype.setJobs = function(value) { + jspb.Message.setProto3IntField(this, 14, value); +}; + + /** * Generated by JsPbCodeGenerator. diff --git a/arduino-ide-extension/src/node/config-service-impl.ts b/arduino-ide-extension/src/node/config-service-impl.ts new file mode 100644 index 00000000..f869fd9a --- /dev/null +++ b/arduino-ide-extension/src/node/config-service-impl.ts @@ -0,0 +1,14 @@ +import { injectable, inject } from "inversify"; +import { ConfigService, Config } from "../common/protocol/config-service"; +import { ArduinoCli } from "./arduino-cli"; + +@injectable() +export class ConfigServiceImpl implements ConfigService { + + @inject(ArduinoCli) + protected readonly cli: ArduinoCli; + + async getConfiguration(): Promise { + return this.cli.getDefaultConfig(); + } +} \ No newline at end of file diff --git a/arduino-ide-extension/src/node/core-client-provider-impl.ts b/arduino-ide-extension/src/node/core-client-provider-impl.ts index f259259b..a4dba8b9 100644 --- a/arduino-ide-extension/src/node/core-client-provider-impl.ts +++ b/arduino-ide-extension/src/node/core-client-provider-impl.ts @@ -1,5 +1,12 @@ -import { inject, injectable } from 'inversify'; +import * as fs from 'fs'; +import * as path from 'path'; import * as grpc from '@grpc/grpc-js'; +import * as PQueue from 'p-queue'; +import { inject, injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { FileSystem } from '@theia/filesystem/lib/common'; +import { WorkspaceServiceExt } from '../browser/workspace-service-ext'; +import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; import { ArduinoCoreClient } from './cli-protocol/commands/commands_grpc_pb'; import { InitResp, @@ -10,16 +17,10 @@ import { UpdateLibrariesIndexReq, UpdateLibrariesIndexResp } from './cli-protocol/commands/commands_pb'; -import { WorkspaceServiceExt } from '../browser/workspace-service-ext'; -import { FileSystem } from '@theia/filesystem/lib/common'; -import URI from '@theia/core/lib/common/uri'; -import { CoreClientProvider, Client } from './core-client-provider'; -import * as PQueue from 'p-queue'; -import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; +import { ArduinoCli } from './arduino-cli'; import { Instance } from './cli-protocol/commands/common_pb'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import * as os from 'os'; +import { CoreClientProvider, Client } from './core-client-provider'; +import { FileUri } from '@theia/core/lib/node'; @injectable() export class CoreClientProviderImpl implements CoreClientProvider { @@ -36,6 +37,9 @@ export class CoreClientProviderImpl implements CoreClientProvider { @inject(ToolOutputServiceServer) protected readonly toolOutputService: ToolOutputServiceServer; + @inject(ArduinoCli) + protected readonly cli: ArduinoCli; + async getClient(workspaceRootOrResourceUri?: string): Promise { return this.clientRequestQueue.add(() => new Promise(async resolve => { const roots = await this.workspaceServiceExt.roots(); @@ -76,19 +80,26 @@ export class CoreClientProviderImpl implements CoreClientProvider { throw new Error(`Could not resolve filesystem path of URI: ${rootUri}.`); } - const defaultDownloadsDirPath = path.resolve(os.homedir(), 'Arduino-PoC', 'downloads'); - if (!fs.existsSync(defaultDownloadsDirPath)) { - fs.mkdirpSync(defaultDownloadsDirPath); + const { dataDirUri, sketchDirUri } = await this.cli.getDefaultConfig(); + const dataDirPath = FileUri.fsPath(dataDirUri); + const sketchDirPath = FileUri.fsPath(sketchDirUri); + + if (!fs.existsSync(dataDirPath)) { + fs.mkdirSync(dataDirPath); } - const defaultDataDirPath = path.resolve(os.homedir(), 'Arduino-PoC', 'data') - if (!fs.existsSync(defaultDataDirPath)) { - fs.mkdirpSync(defaultDataDirPath); + if (!fs.existsSync(sketchDirPath)) { + fs.mkdirSync(sketchDirPath); } - config.setSketchbookdir(rootPath); - config.setDatadir(defaultDataDirPath); - config.setDownloadsdir(defaultDownloadsDirPath); + const downloadDir = path.join(dataDirPath, 'staging'); + if (!fs.existsSync(downloadDir)) { + fs.mkdirSync(downloadDir); + } + + config.setSketchbookdir(sketchDirPath); + config.setDatadir(dataDirPath); + config.setDownloadsdir(downloadDir); config.setBoardmanageradditionalurlsList(['https://downloads.arduino.cc/packages/package_index.json']); const initReq = new InitReq(); diff --git a/arduino-ide-extension/src/node/daemon-log.ts b/arduino-ide-extension/src/node/daemon-log.ts index 5c452881..0824ffe8 100644 --- a/arduino-ide-extension/src/node/daemon-log.ts +++ b/arduino-ide-extension/src/node/daemon-log.ts @@ -8,7 +8,58 @@ export interface DaemonLog { export namespace DaemonLog { - export type Level = 'info' | 'debug' | 'warning' | 'error'; + export interface Url { + readonly Scheme: string; + readonly Host: string; + readonly Path: string; + } + + export namespace Url { + + export function is(arg: any | undefined): arg is Url { + return !!arg + && typeof arg.Scheme === 'string' + && typeof arg.Host === 'string' + && typeof arg.Path === 'string'; + } + + export function toString(url: Url): string { + const { Scheme, Host, Path } = url; + return `${Scheme}://${Host}${Path}`; + } + + } + + export interface System { + readonly os: string; + // readonly Resource: Resource; + } + + export namespace System { + export function toString(system: System): string { + return `OS: ${system.os}` + } + } + + export interface Tool { + readonly version: string; + readonly systems: System[]; + } + + export namespace Tool { + + export function is(arg: any | undefined): arg is Tool { + return !!arg && typeof arg.version === 'string' && 'systems' in arg; + } + + export function toString(tool: Tool): string { + const { version, systems } = tool; + return `Version: ${version}${!!systems ? ` Systems: [${tool.systems.map(System.toString).join(', ')}]` : ''}`; + } + + } + + export type Level = 'trace' | 'debug' | 'info' | 'warning' | 'error'; export function is(arg: any | undefined): arg is DaemonLog { return !!arg @@ -20,61 +71,62 @@ export namespace DaemonLog { export function toLogLevel(log: DaemonLog): LogLevel { const { level } = log; switch (level) { - case 'info': return LogLevel.INFO; + case 'trace': return LogLevel.TRACE; case 'debug': return LogLevel.DEBUG; - case 'error': return LogLevel.ERROR; + case 'info': return LogLevel.INFO; case 'warning': return LogLevel.WARN; + case 'error': return LogLevel.ERROR; default: return LogLevel.INFO; } } - export function log(logger: ILogger, toLog: string): void { - const segments = toLog.split('time').filter(s => s.trim().length > 0); - for (const segment of segments) { - const maybeDaemonLog = parse(`time${segment}`.trim()); - for (const logMsg of maybeDaemonLog) { - logger.log(toLogLevel(logMsg), logMsg.msg); - } + export function log(logger: ILogger, logMessages: string): void { + const parsed = parse(logMessages); + for (const log of parsed) { + logger.log(toLogLevel(log), toMessage(log)); } } - // Super naive. function parse(toLog: string): DaemonLog[] { - const messages = toLog.split('\ntime='); + const messages = toLog.trim().split('\n'); const result: DaemonLog[] = []; for (let i = 0; i < messages.length; i++) { - const msg = (i > 0 ? 'time=' : '') + messages[i]; - const rawSegments = msg.split(/(\s+)/) - .map(segment => segment.replace(/['"]+/g, '')) - .map(segment => segment.trim()) - .filter(segment => segment.length > 0); - - const timeIndex = rawSegments.findIndex(segment => segment.startsWith('time=')); - const levelIndex = rawSegments.findIndex(segment => segment.startsWith('level=')); - const msgIndex = rawSegments.findIndex(segment => segment.startsWith('msg=')); - if (rawSegments.length > 2 - && timeIndex !== -1 - && levelIndex !== -1 - && msgIndex !== -1) { - result.push({ - time: rawSegments[timeIndex].split('=')[1], - level: rawSegments[levelIndex].split('=')[1] as Level, - msg: [rawSegments[msgIndex].split('=')[1], ...rawSegments.slice(msgIndex + 1)].join(' ') - }); - } else { + try { + const maybeDaemonLog = JSON.parse(messages[i]); + if (DaemonLog.is(maybeDaemonLog)) { + result.push(maybeDaemonLog); + continue; + } + } catch { /* NOOP */ } result.push({ - time: new Date().toString(), - level: 'info', - msg: msg + time: new Date().toString(), + level: 'info', + msg: messages[i] }); - } } - // Otherwise, log the string as is. return result; } - export function toPrettyString(logMessage: string): string { - const parsed = parse(logMessage); - return parsed.map(msg => `[${msg.level.toUpperCase() || 'INFO'}] ${msg.msg}\n`).join(''); + export function toPrettyString(logMessages: string): string { + const parsed = parse(logMessages); + return parsed.map(toMessage).join('\n'); } + + function toMessage(log: DaemonLog): string { + const details = Object.keys(log).filter(key => key !== 'msg' && key !== 'level' && key !== 'time').map(key => toDetails(log, key)).join(', '); + return `[${log.level.toUpperCase()}] ${log.msg}${!!details ? ` [${details}]` : ''}` + } + + function toDetails(log: DaemonLog, key: string): string { + let value = (log as any)[key]; + if (DaemonLog.Url.is(value)) { + value = DaemonLog.Url.toString(value); + } else if (DaemonLog.Tool.is(value)) { + value = DaemonLog.Tool.toString(value); + } else if (typeof value === 'object') { + value = JSON.stringify(value).replace(/\"([^(\")"]+)\":/g, '$1:'); // Remove the quotes from the property keys. + } + return `${key.toLowerCase()}: ${value}`; + } + } \ No newline at end of file diff --git a/arduino-ide-extension/src/node/default-workspace-server-ext.ts b/arduino-ide-extension/src/node/default-workspace-server-ext.ts index c747408e..70a0a66b 100644 --- a/arduino-ide-extension/src/node/default-workspace-server-ext.ts +++ b/arduino-ide-extension/src/node/default-workspace-server-ext.ts @@ -1,14 +1,15 @@ -import * as os from 'os'; -import * as path from 'path'; -import { injectable } from 'inversify'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { injectable, inject } from 'inversify'; import { DefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server'; +import { ConfigService } from '../common/protocol/config-service'; @injectable() export class DefaultWorkspaceServerExt extends DefaultWorkspaceServer { + @inject(ConfigService) protected readonly configService: ConfigService; + protected async getWorkspaceURIFromCli(): Promise { - return FileUri.create(path.join(os.homedir(), 'Arduino-PoC', 'Sketches')).toString(); + const config = await this.configService.getConfiguration(); + return config.sketchDirUri; } } \ No newline at end of file diff --git a/arduino-ide-browser/package.json b/browser-app/package.json similarity index 93% rename from arduino-ide-browser/package.json rename to browser-app/package.json index afa5811d..d016f717 100644 --- a/arduino-ide-browser/package.json +++ b/browser-app/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "arduino-ide-browser", + "name": "browser-app", "version": "0.0.1", "license": "MIT", "dependencies": { @@ -30,7 +30,7 @@ "theia": { "frontend": { "config": { - "applicationName": "Arduino-PoC", + "applicationName": "Arduino Editor", "defaultTheme": "arduino-theme", "preferences": { "editor.autoSave": "on" diff --git a/arduino-ide-electron/package.json b/electron-app/package.json similarity index 94% rename from arduino-ide-electron/package.json rename to electron-app/package.json index f3392091..b7d19c3a 100644 --- a/arduino-ide-electron/package.json +++ b/electron-app/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "arduino-ide-electron", + "name": "electron-app", "version": "0.0.1", "license": "MIT", "dependencies": { @@ -33,7 +33,7 @@ "target": "electron", "frontend": { "config": { - "applicationName": "Arduino-PoC", + "applicationName": "Arduino Editor", "defaultTheme": "arduino-theme", "preferences": { "editor.autoSave": "on" diff --git a/electron/packager/index.js b/electron/packager/index.js index 0307df8c..330a4720 100644 --- a/electron/packager/index.js +++ b/electron/packager/index.js @@ -14,7 +14,7 @@ const workingCopy = 'working-copy'; /** - * Relative path from the `__dirname` to the root where the `arduino-ide-extension` and the `arduino-ide-electron` folders are. + * Relative path from the `__dirname` to the root where the `arduino-ide-extension` and the `electron-app` folders are. * This could come handy when moving the location of the `electron/packager`. */ const rootPath = join('..', '..'); @@ -42,18 +42,18 @@ // Copy the following items into the `working-copy` folder. Make sure to reuse the `yarn.lock`. | //----------------------------------------------------------------------------------------------+ mkdir('-p', path('..', workingCopy)); - for (const name of ['arduino-ide-extension', 'arduino-ide-electron', 'yarn.lock', 'package.json', 'lerna.json']) { + for (const name of ['arduino-ide-extension', 'electron-app', 'yarn.lock', 'package.json', 'lerna.json']) { cp('-rf', path(rootPath, name), path('..', workingCopy)); } //-----------------------------------------------------+ - // No need to build the `arduino-ide-browser` example. | + // No need to build the `browser-app` example. | //-----------------------------------------------------+ //@ts-ignore let pkg = require('../working-copy/package.json'); const workspaces = pkg.workspaces; - // We cannot remove the `arduino-ide-electron`. Otherwise, there is not way to collect the unused dependencies. - const dependenciesToRemove = ['arduino-ide-browser']; + // We cannot remove the `electron-app`. Otherwise, there is not way to collect the unused dependencies. + const dependenciesToRemove = ['browser-app']; for (const dependencyToRemove of dependenciesToRemove) { const index = workspaces.indexOf(dependencyToRemove); if (index !== -1) { @@ -70,13 +70,13 @@ // Collect all unused dependencies by the backend. We have to remove them from the electron app. // The `bundle.js` already contains everything we need for the frontend. // We have to do it before changing the dependencies to `local-path`. - const unusedDependencies = await utils.collectUnusedDependencies('../working-copy/arduino-ide-electron/'); + const unusedDependencies = await utils.collectUnusedDependencies('../working-copy/electron-app/'); //------------------------------------------------------------------------------------+ // Merge the `working-copy/package.json` with `electron/build/template-package.json`. | //------------------------------------------------------------------------------------+ // @ts-ignore - pkg = require('../working-copy/arduino-ide-electron/package.json'); + pkg = require('../working-copy/electron-app/package.json'); // @ts-ignore const template = require('../build/template-package.json'); template.build.files = [ ...template.build.files, ...unusedDependencies.map(name => `!node_modules/${name}`) ]; diff --git a/package.json b/package.json index 86a71718..f011e244 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "arduino-poc", + "name": "arduino-editor", "version": "0.0.1", - "description": "A PoC demonstrating an Arduino IDE built using Eclipse Theia", + "description": "Arduino IDE built using Eclipse Theia", "main": "index.js", "repository": "https://github.com/bcmi-labs/arduino-editor.git", "author": "Christian Weichel ", @@ -14,12 +14,12 @@ "prepare": "lerna run prepare", "rebuild:browser": "theia rebuild:browser", "rebuild:electron": "theia rebuild:electron", - "start": "cd arduino-ide-browser && yarn start", + "start": "yarn --cwd ./browser-app start", "watch": "lerna run watch --parallel" }, "workspaces": [ - "arduino-ide-electron", - "arduino-ide-browser", - "arduino-ide-extension" + "arduino-ide-extension", + "electron-app", + "browser-app" ] } diff --git a/yarn.lock b/yarn.lock index cdc8943d..ae96ea90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4473,7 +4473,7 @@ decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: is-stream "^1.1.0" tar-stream "^1.5.2" -decompress-tarbz2@^4.0.0, decompress-tarbz2@^4.1.1: +decompress-tarbz2@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== @@ -4484,7 +4484,7 @@ decompress-tarbz2@^4.0.0, decompress-tarbz2@^4.1.1: seek-bzip "^1.0.5" unbzip2-stream "^1.0.9" -decompress-targz@^4.0.0: +decompress-targz@^4.0.0, decompress-targz@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== @@ -8098,7 +8098,7 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@^2.10.6, moment@^2.18.1, moment@^2.21.0: +moment@^2.10.6, moment@^2.18.1, moment@^2.21.0, moment@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==