Stefan Winkler's Blog
This article is part of a series to help newcomers to get started with Eclipse Theia development by setting up a local docker-based development environment for Theia.
In the previous article, we have created our first Eclipse Theia extension, changed some code, and used the debugger to set and hit a breakpoint in the frontend.
As a next step, I wanted to demonstrate how to debug the backend. So the best way, I thought, was to create a backend service to provide the "Hello World" message instead of having it hard-coded in the frontend.
As described in the Theia Documentation, a Theia Extension can add logic to both the frontend and the backend. To facilitate communication between both components, any protocol could be used; the backend could register and open its own endpoint and the frontend could access it. But, of course, Theia already provides a JSON-RPC API which can be used quite easily.
All of the code below is available in the hello-world branch of my GitHub repository.
Let’s start by specifying a service which can provide our "Hello World“ message. Since it needs to be known by both the backend and the frontend, we put it in hello-world/src/common/hello-world.ts
:
export namespace HelloWorldConstants {
export const SERVICE_PATH = '/services/hello-world';
}
export const HelloWorld = Symbol("HelloWorld")
export interface HelloWorld {
getHelloString(): Promise<string>;
}
This code defines an interface for our very simple service, a Symbol for it (which is required by the dependency injection framework), and a constant for the service path at which we want to publish/consume our service. Note that the service returns a Promise<string> instead of a plain string. Since we are dealing with a remote service, using promises makes the code behave better, because we can consume the result asynchronously as we receive it, as we will see below.
The service implementation in the backend goes into hello-world/src/node/hello-world-impl.ts
and is as simple as:
@injectable()
export class HelloWorldImpl implements HelloWorld {
getHelloString(): Promise<string> {
return Promise.resolve("Hello from the backend!");
}
}
We need to annotate the class with @injectable() because we want to use it as an injection binding in the backend module later.
Now, that we have the service and its implementation, let’s use it from the CommandContribution in the frontend (I am only showing the changed class HelloWorldCommandContribution):
@injectable()
export class HelloWorldCommandContribution implements CommandContribution {
constructor(
@inject(MessageService) private readonly messageService: MessageService,
@inject(HelloWorld) private readonly helloWorldService: HelloWorld
) { }
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(HelloWorldCommand, {
execute: async () => {
this.helloWorldService.getHelloString().then(helloString => this.messageService.info(helloString))
}
});
}
}
Note that we have added an injection for the HelloWorld service, and in the execute logic, we chain the Promise with the callback logic via the then() function. So, we request the helloString asynchronously, and as soon as it is received (and the Promise is resolved), we call the messageService to show it.
The next step is to tell the dependency injection framework in the frontend how to provide the HelloWorld service we want to inject and use in the HelloWorldCommandContribution. To do this, we extend the existing hello-world-frontend-module.ts
as follows:
export default new ContainerModule(bind => {
// add your contribution bindings here
bind(CommandContribution).to(HelloWorldCommandContribution);
bind(MenuContribution).to(HelloWorldMenuContribution);
bind(HelloWorld).toDynamicValue(ctx => {
const connection = ctx.container.get(WebSocketConnectionProvider);
return connection.createProxy<HelloWorld>(HelloWorldConstants.SERVICE_PATH);
}).inSingletonScope();
});
What we do here is to create a proxy implementation of the HelloWorld interface that is backed by a WebSocketConnectionProvider, which in turn is instructed to handle requests via the SERVICE_PATH path. Every method call on the proxy is encoded in a JSON-RPC request and sent to the backend via the given SERVICE_PATH.
At the backend-side in hello-world/src/node/hello-world-backend-module.ts
, we create and register the peer instance:
export default new ContainerModule(bind => {
bind(HelloWorld).to(HelloWorldImpl).inSingletonScope();
bind(ConnectionHandler).toDynamicValue(ctx =>
new JsonRpcConnectionHandler<HelloWorld>(HelloWorldConstants.SERVICE_PATH, (_client: any) => ctx.container.get<HelloWorld>(HelloWorld))
).inSingletonScope()
})
First, we bind the HelloWorld service to its actual implementation HelloWorldImpl. This is not strictly required for our use case, but would make sense as soon as HelloWorldImpl wanted to access other services which are provided via injection.
Next, we create the JsonRpcConnectionHandler which is the counterpart of the WebSocketConnectionProvider above. Like in the frontend, we bind the WebSocketConnectionProvider to the SERVICE_PATH. All incoming method invocation requests are then forwarded to a HelloWorldServiceImpl instance. (Note that there is also a _client argument. We don’t use it here, but we could use it to implement a bi-directional protocol in which the client can be called back by the server).
Now the only thing left to do is to register the backend ContainerModule configuration in the package.json
file
"theiaExtensions": [
{
"frontend": "lib/browser/hello-world-frontend-module",
"backend": "lib/node/hello-world-backend-module"
}
]
and we are all set. When we launch first the backend, then the frontend in the browser and click on the Say Hello menu item, the new message will appear: Hello from the backend!
Debugging
Backend debugging is easier than frontend debugging described in the previous article, because we don’t need to deal with a browser debugging engine. The VS Code debugger can natively attach to the backend process and the yeoman code generator already took care of creating a launch configuration for us in launch.json. So, we can just place a breakpoint in the getHelloString() method in the HelloWorldImpl class, launch the Launch Backend configuration, and when we click the menu item in the frontend, we see that the breakpoint is hit.
Besides plain breakpoints, VS Code also supports conditional breakpoints, breakpoints with hit counts, and logpoints. The latter are really useful when trying to find out which part of code is called when. Using a logpoint, you can inject an arbitrary log statement that is executed without the debugger suspending the execution.
To try it out, let’s add a logpoint in the node_modules/@theia/core/src/common/messaging/proxy-factory.ts
at line 156 in the onRequest method by right-clicking on the line number and selecting Add Logpoint .... As the expression we enter Request: {method}
. The expression inside the curly braces is evaluated when the breakpoint is hit.
Now, we can play around with the frontend and can see the requests that are issues to the backend:
Note: At the time of writing this, there is a bug in the VS Code JavaScript debugger that prevents logpoints from working correctly with Theia. This bug is already fixed when installing the vscode-js-debug nightly as described here, and will hopefully be resolved soon in the next VS Code release.
And with this, I close today’s article. Have fun playing around with logpoints.
- Details
- Category: Eclipse
This article is part of a series to help newcomers to get started with Eclipse Theia development by setting up a local docker-based development environment for Theia.
After setting up a Theia Development environment (either with Docker Dev Environments or VS Code Remote Containers), the next step is to actually write some code.
This article provides a good description of the process of creating a first extension with the help of a yeoman code generator. This code generator actually takes care of creating both the extension code and a custom theia product. So we don’t even have to use the package.json
from my previous articles. But since we have already a development container set up, we will just reuse the existing environment here.
So let’s just ignore the existing files, open a Terminal in our Docker workspace and create a new folder hello-world
. Then we change into the new folder with and invoke the code generator:
$ mkdir hello-world
$ cd hello-world
$ yo theia-extension
In the dialog that follows, we select the Hello World extension and just accept the proposed name.
Now, first the code generator and then yarn
will do their thing again to get the new Theia instance set up and resolved, and in the end, we can start theia by executing yarn start:browser
.
In the Theia application that is running now, note that we have a new menu item Say Hello in the Edit Menu. When selecting this item, a message in the lower right says Hello World.
Let’s try to modify the message. But first, in order to have our our code changes be picked up, we need to tell the TypeScript compiler to watch the code. Watching means, that any change to the code in a .ts file is directly picked up and compiled to JavaScript. For developers being used to the Eclipse IDE, this is the equivalent of activating Project > Build Automatically. Open up a New Terminal, and–as we will only make changes to the hello-world extension–change to the folder of our hello-world extension and execute yarn watch
from there. Restricting the watch to the extension folder will save resources, as only the files in that folder need to be watched.
Note: If
yarn watch
complains about insufficient file handles for watching ("System limit for number of file watchers reached“), then you need to adjust the number of available handles in /etc/sysctl.conf on your host system (e.g., for me it was the boot2docker vm in which I run my docker-machines). For details, see this StackOverflow answer.
Now find and open the hello-world-contribution.ts file in the VS Code editor and change the message in line 19 to Hello changed world!
. Save the file, and you will briefly see the console blink as it picks up the changes and reports "Found 0 errors".
Finally, we need to reload the Theia page in the browser if it still open. The reason is that the code that produces the message was compiled from TypeScript to JavaScript, but as it is frontend code, it has actually been loaded and is executed in our web browser.
Debugging
Executing and changing code is one thing, but sooner or later, we will want to inspect what is going on at runtime by using a debugger. So, let us create a breakpoint at the line in which we have changed the message and try to hit it.
Note that, as stated above, this line is part of the frontend code, so we need to attach a debugger to the frontend, which means the browser. So, we could use the builtin development tooling that is part of all web browsers, nowadays. But the browser does not know about TypeScript and the TypeScript compiler; it only knows about JavaScript which it loads and executes. Consequently, we would not be able to hit the breakpoint set in VS Code. Instead, we would need to find out which line in JavaScript relates to the line in TypeScript, and set a breakpoint in this line in the browser’s development tools. In practice this seems to be cumbersome and error-prone.
Correction: Actually, the typescript sources and source maps are included by webpack when assembling the assets to be downloaded to the browser. This means that in the browser development tools, there should be a webpack:// node visible which contains the .ts files; and it is actually possible to set breakpoints there and use the browser development tools to debug Theia in the browser directly. So if you are familiar with the browser development tools, or encounter issues when trying to attach VS Code to the browser as described below, this is a good alternative to debug the frontend.
Luckily, VS Code also has a frontend debugging feature that is able to attach to a debug server built into Google Chrome or Microsoft Edge browsers. The setup is quite simple: Switch to the Run and Debug view in the Activity Bar and click the Create launch.json File link. Then select the Chrome environment and adapt the file to contain
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/hello-world/hello-world",
"runtimeExecutable": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
}
]
}
Note that the url property needs to set so it points to the port at localhost on which Theia is running. The webRoot property needs to point to the actual path in the workspace where the code of the extension is located. This is used to connect the breakpoint position to the browser’s debug server. If this property is set to a wrong location, you will see it marked as Unbound while the debugger is run.
Also note that the runtimeExecutable might not be necessary, but for me it was needed–otherwise launching the browser executable would fail without giving a specific error message.
Now, all we have to do is make sure that Theia (i.e., yarn start:browser
) is still running and start the Launch Chrome against localhost Debug Configuration. Now click on the „Say Hello“ menu item again, and the execution should suspend and the debugger should show the stack, variables, etc. and we can use the debugger to step through the code as it is executed.
That’s it for today. As the hello-world extension does not include any backend code, we will postpone backend debugging to a future article.
- Details
- Category: Eclipse
This article is part of a series to help newcomers to get started with Eclipse Theia development by setting up a local docker-based development environment for Theia.
After having written the previous article Getting started with Eclipse Theia (1): Starting Theia Development in a Docker Dev Environment, I have tried the same setup on a different machine which does not have a Docker Desktop installation, but which uses docker-machine to manage its containers in a boot2docker VM.
My goal was to achieve a similarly easy startup on this machine (and also on Linux machines for which Docker Desktop is not available). By digging a bit deeper into the inner workings of the Docker Dev Environments feature, I have learned that most of the functionality is also available in VS Code directly via the Remote Containers Extension.
Similar to the Docker Dev Environments feature, you can add a folder .devcontainer
to a repository containing a file called devcontainer.json
. Then, anyone can directly clone this repository and start working on it inside a docker container with VS Code. This is how my devcontainer.json
file looks like:
{
"name": "theia-dev",
"context": "../docker-container",
"dockerFile": "../docker-container/Dockerfile",
"forwardPorts": [3000],
"postCreateCommand": "yarn && yarn theia build",
"remoteUser": "node"
}
The Dockerfile
referenced here is the same we have used for the Docker Dev Environments in the last article.
To get started, after installing the Remote Containers Extension in VS Code, follow these steps:
- From the Command Palette (F3), run "Remote-Containers: Clone Repository in Container Volume“.
- Enter the URL to the repository (or clone my repository
github.com/xpomul/theia-boilerplate.git
) and follow the navigation to select your GitHub repository - Now wait. The container will be built, the repository will be cloned in a volume, and even the
yarn
andyarn theia build
commands will be executed automatically (as specified in thedevcontainer.json
). - After everything is done, you can close the Terminal, open a new one and just run
yarn theia start --plugins=local-dir:plugins
and your Eclipse Theia instance will come up athttp://localhost:3000
Again, from here, you can follow the usual Theia tutorials and start experimenting.
Have fun!
- Details
- Category: Eclipse
This article is part of a series to help newcomers to get started with Eclipse Theia development by setting up a local docker-based development environment for Theia.
So here it is. After over 10 years of Eclipse development, I wanted to get my hands dirty on something entirely new. I have followed the developments of Orion, Che, and Theia for a long time, but only from a distance; and I just didn’t have the time to actually try out, let alone to get involved with, these projects.
I have used Gitpod a few times, but merely to have a quick look into a GitHub repository when I didn’t have a local development environment and didn’t want to get one up and running.
Yet, I have noticed an increasing interest from my customers in web-based environments and so, I wanted to finally play around with Eclipse Theia.
But how? I have found this article which provides several options to run Eclipse Theia in different ways, but none of those options really hit the spot for me. I wanted to run a Theia instance in my browser, so Theia Blueprint was not an option. I wanted to be able to work offline, so using Gitpod was not possible. And I didn’t want to install nodejs on my machine. But still the goal was to get a basic Theia product up and running that should be ready to start hacking and customizing it, and then to jump in and create my first Theia extension.
I had started, and aborted again, several attempts to create a Docker container in which I could do that. But so far, I had not managed to create a smooth environment.
- Details
- Category: Eclipse