Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: support custom port for website resource and simulator #334

Merged
merged 2 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/chilly-crabs-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@plutolang/simulator-adapter": patch
"@plutolang/cli": patch
---

feat(simulator): support custom address configuration
8 changes: 8 additions & 0 deletions .changeset/strange-boxes-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@plutolang/pluto-infra": patch
"@plutolang/pluto": patch
---

feat(sdk): add support for configuring host and port for website resource

This change introduces the ability to specify custom host and port settings for website resources, enhancing flexibility during local development.
1 change: 1 addition & 0 deletions apps/cli/src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ async function executeOnce(project: config.Project, stack: config.Stack, entrypo
archRef: archRef,
entrypoint: "",
stateDir: stateDir,
extraConfigs: project.configs,
});
await deployWithAdapter(adapter, stack);
}
Expand Down
7 changes: 6 additions & 1 deletion components/adapters/simulator/src/simAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ export class SimulatorAdapter extends core.Adapter {
WORK_DIR: this.stateDir,
};

this.simulator = new Simulator(this.rootpath);
let address: string | undefined;
if (this.extraConfigs?.simulator) {
address = this.extraConfigs.simulator.address;
}

this.simulator = new Simulator(this.rootpath, address);
await this.simulator.start();
envs.PLUTO_SIMULATOR_URL = this.simulator.serverUrl;

Expand Down
40 changes: 28 additions & 12 deletions components/adapters/simulator/src/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { ComputeClosure, AnyFunction, createClosure } from "@plutolang/base/clos
import { MethodNotFound, ResourceNotFound } from "./errors";

export class Simulator {
private readonly projectRoot: string;

private resources: Map<string, simulator.IResourceInstance>;
private closures: Map<string, ComputeClosure<AnyFunction>>;

Expand All @@ -18,8 +16,10 @@ export class Simulator {

private readonly exitHandler = async () => {};

constructor(projectRoot: string) {
this.projectRoot = projectRoot;
constructor(
private readonly projectRoot: string,
private readonly address?: string
) {
this.resources = new Map();
this.closures = new Map();

Expand Down Expand Up @@ -212,19 +212,35 @@ export class Simulator {

public async start(): Promise<void> {
const expressApp = this.createExpress();
for (let port = 9001; ; port++) {
const server = await tryListen(expressApp, port);

if (this.address) {
const [host, port] = this.address.split(":");
const server = await tryListen(expressApp, parseInt(port), host);
if (server === undefined) {
continue;
throw new Error(`Failed to listen on ${this.address}`);
}

const addr = server.address();
if (addr && typeof addr === "object" && addr.port) {
this._serverUrl = `http://${addr.address}:${addr.port}`;
}
this._serverUrl = `http://${host}:${port}`;
this._server = server;
} else {
if (process.env.DEBUG) {
console.log("Starting simulator on a random port...");
}

for (let port = 9001; ; port++) {
const server = await tryListen(expressApp, port);
if (server === undefined) {
continue;
}

break;
const addr = server.address();
if (addr && typeof addr === "object" && addr.port) {
this._serverUrl = `http://localhost:${addr.port}`;
}
this._server = server;

break;
}
}
}

Expand Down
71 changes: 55 additions & 16 deletions packages/pluto-infra/src/simulator/website.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,88 @@
import fs from "fs";
import http from "http";
import path from "path";
import cors from "cors";
import express from "express";
import { IResourceInfra } from "@plutolang/base";
import { IWebsiteClient, IWebsiteInfra, Website, WebsiteOptions } from "@plutolang/pluto";
import { genResourceId } from "@plutolang/base/utils";

/**
* Adapts the options to the correct names for TypeScript.
* The option names for TypeScript and Python are different, so this function converts Python-style
* option names to TypeScript-style option names.
*
* @param opts - The options object that may contain Python-style option names.
* @returns The adapted options object with TypeScript-style option names.
*/
function adaptOptions(opts?: any): WebsiteOptions | undefined {
if (opts === undefined) {
return;
}

if (opts.sim_host) {
opts.simHost = opts.sim_host;
}
if (opts.sim_port) {
opts.simPort = opts.sim_port;
}
return opts;
}

export class SimWebsite implements IResourceInfra, IWebsiteInfra, IWebsiteClient {
public id: string;
private websiteDir: string;

private envs: { [key: string]: string } = {};
private originalPlutoJs: string | undefined;

private expressApp: express.Application;
private httpServer: http.Server;
private host: string;
private port: number;

public outputs: string;
public outputs?: string;

constructor(path: string, name?: string, options?: WebsiteOptions) {
name = name ?? "default";
options = adaptOptions(options);

this.id = genResourceId(Website.fqn, name);
this.websiteDir = path;

this.expressApp = express();
this.expressApp.use(cors());
this.expressApp.use(express.static(this.websiteDir));
this.host = options?.simHost ?? "localhost";
this.port = parseInt(options?.simPort ?? "0");
}

this.httpServer = this.expressApp.listen(0);
const address = this.httpServer.address();
if (address && typeof address !== "string") {
this.port = address.port;
} else {
throw new Error(`Failed to obtain the port for the router: ${name}`);
public async init() {
const expressApp = express();
expressApp.use(cors());
expressApp.use(express.static(this.websiteDir));

const httpServer = expressApp.listen(this.port, this.host);

async function waitForServerReady() {
return await new Promise<number>((resolve, reject) => {
httpServer.on("listening", () => {
const address = httpServer.address();
if (address && typeof address !== "string") {
resolve(address.port);
} else {
reject(new Error(`Failed to obtain the port for the router`));
}
});

httpServer.on("error", (err) => {
console.error(err);
reject(err);
});
});
}
const realPort = await waitForServerReady();
this.port = realPort;

this.outputs = this.url();

options;
}

public url(): string {
return `http://localhost:${this.port}`;
return `http://${this.host}:${this.port}`;
}

public addEnv(key: string, value: string): void {
Expand Down
10 changes: 9 additions & 1 deletion packages/pluto-infra/src/utils/impl-class-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ export class ImplClassMap<T, K extends new (...args: any[]) => T> {
...args: any[]
): Promise<T> {
const implClass = await this.loadImplClassOrThrow(platformType, provisionType);
return new implClass(...args);

const instance = new implClass(...args);

// If there is the `init` method in the implementation class, use it to initialize the instance.
if (typeof implClass.prototype.init === "function") {
await (instance as any).init();
}

return instance;
}
}
12 changes: 12 additions & 0 deletions packages/pluto-py/pluto_client/universal_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import os
from typing import Any


class UniversalClass:
def __getattr__(self, name: str):
class MethodCaller:
def __call__(self, *args: Any, **kwargs: Any):
if os.getenv("DEBUG", False):
print(f"Method called: {name}")
return None
return MethodCaller()
16 changes: 15 additions & 1 deletion packages/pluto-py/pluto_client/website.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional
from dataclasses import dataclass
from pluto_base import utils
from pluto_base.platform import PlatformType
Expand All @@ -9,6 +9,8 @@
IResourceInfraApi,
)

from .universal_class import UniversalClass


@dataclass
class WebsiteOptions:
Expand All @@ -17,6 +19,16 @@ class WebsiteOptions:
Currently, only support Vercel. If an invalid value is provided, or if no value is provided at
all, it will default to your specified platform.
"""
sim_host: Optional[str] = None
"""
Host address for simulating the website when running the project with `pluto run`. If not
provided, it will be `localhost`.
"""
sim_port: Optional[str] = None
"""
Port number for simulating the website when running the project with `pluto run`. If not
provided, it will be randomly assigned.
"""


class IWebsiteClientApi(IResourceClientApi):
Expand Down Expand Up @@ -55,5 +67,7 @@ def __init__(
from .clients import shared

self._client = shared.WebsiteClient(path, name, opts)
if platform_type in [PlatformType.Simulator]:
self._client = UniversalClass() # type: ignore
else:
raise ValueError(f"not support this runtime '{platform_type}'")
12 changes: 12 additions & 0 deletions packages/pluto/src/website.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ export interface WebsiteOptions {
* all, it will default to your specified platform.
*/
platform?: "Vercel";

/**
* Host address for simulating the website when running the project with `pluto run`. If not
* provided, it will be `localhost`.
*/
simHost?: string;

/**
* Port number for simulating the website when running the project with `pluto run`. If not
* provided, it will be a random port.
*/
simPort?: string;
}

/**
Expand Down
Loading