Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public static partial class Features
public static partial class PortalExtension
{
public const string EXTENSION_DEPENDENCIES_PROPERTY_NAME = "microserviceDependencies";
public const string EXTENSION_BEAMABLE_PROPERTY_NAME = "beamable";
public const string EXTENSION_NAME_PROPERTY_NAME = "name";
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"noEmit": true,
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"]
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "beamable/**/*.ts"]
}
23 changes: 18 additions & 5 deletions cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Beamable.Server;
using cli.Services;
using cli.Services.Web;
using Newtonsoft.Json.Linq;
using System.CommandLine;
using static Beamable.Common.Constants.Features.PortalExtension;

namespace cli.Portal;

Expand Down Expand Up @@ -62,7 +64,13 @@ public override Task Handle(PortalExtensionAddDependencyCommandArgs args)

extension.MicroserviceDependencies.Add(microservice.BeamoId);

root[Beamable.Common.Constants.Features.PortalExtension.EXTENSION_DEPENDENCIES_PROPERTY_NAME] =
if (root[EXTENSION_BEAMABLE_PROPERTY_NAME] == null)
{
throw new CliException(
$"Field {EXTENSION_BEAMABLE_PROPERTY_NAME} expected in extension package.json file");
}

root[EXTENSION_BEAMABLE_PROPERTY_NAME][EXTENSION_DEPENDENCIES_PROPERTY_NAME] =
JToken.FromObject(extension.MicroserviceDependencies);

File.WriteAllText(packagePath, root.ToString(Newtonsoft.Json.Formatting.Indented));
Expand All @@ -86,12 +94,18 @@ public static void GenerateDependenciesClients(string extensionPath, BeamoLocalM

foreach ((string beamId, HttpMicroserviceLocalProtocol localProtocol) in manifest.HttpMicroserviceLocalProtocols)
{
if (!dependencies.Contains(beamId) || localProtocol.OpenApiDoc == null)
if (!dependencies.Contains(beamId))
{
continue;
}

if (localProtocol.OpenApiDoc == null)
{
Log.Warning($"Client generation for {beamId} is being skipped because there is no API doc. Try running {beamId} once to make it available and try again.");
continue;
}

var generator = new WebClientCodeGenerator(localProtocol.OpenApiDoc, "js");
var generator = new WebClientCodeGenerator(localProtocol.OpenApiDoc, "ts");
var clientsOutputDirectory = Path.Combine(extensionPath, "beamable/clients");
generator.GenerateClientCode(clientsOutputDirectory);
}
Expand All @@ -104,11 +118,10 @@ public static List<string> GetDependenciesFromPath(string extensionPath)

JObject root = JObject.Parse(jsonContent);

JToken deps = root[Beamable.Common.Constants.Features.PortalExtension.EXTENSION_DEPENDENCIES_PROPERTY_NAME];
JToken deps = root[EXTENSION_BEAMABLE_PROPERTY_NAME][EXTENSION_DEPENDENCIES_PROPERTY_NAME];

if (deps is { Type: JTokenType.Array })
{
// Convert the JArray directly to a List<string>
return deps.ToObject<List<string>>();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public class ExtensionBuildMetaData
public class PortalExtensionDiscoveryService : Microservice
{

[ClientCallable]
// Note: this creates source code leaking, even tho the service has an ever changing Guid
[Callable]
public ExtensionBuildData RequestPortalExtensionData(string currentHash = "")
{
var observer = Provider.GetService<PortalExtensionObserver>();
Expand Down
6 changes: 3 additions & 3 deletions cli/cli/Services/Web/WebClientCodeGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Beamable.Server;
using Beamable.Server;
using cli.Services.Web.CodeGen;
using cli.Services.Web.Helpers;
using Microsoft.OpenApi.Any;
Expand Down Expand Up @@ -74,7 +74,7 @@ public WebClientCodeGenerator(OpenApiDocument document, string langType)
if (_schemas.Count > 0)
{
var tsBeamSchemaImport =
new TsImport("beamable-sdk/schema", defaultImport: "* as Schemas", typeImportOnly: true);
new TsImport("@beamable/sdk/schema", defaultImport: "* as Schemas", typeImportOnly: true);
_clientFile.AddImport(tsBeamSchemaImport);
}

Expand Down Expand Up @@ -283,7 +283,7 @@ private static void GenerateBeamSDKModuleDeclaration(TsFile clientFile, TsClass
var className = tsClass.Name;
var camelCaseClassName = StringHelper.ToCamelCaseIdentifier(className);
var serviceName = tsClass.Name.Replace("Client", string.Empty);
var tsModule = new TsModule("beamable-sdk");
var tsModule = new TsModule("@beamable/sdk");
var tsInterface = new TsInterface("BeamBase");
var tsProperty = new TsProperty(camelCaseClassName, TsType.Of(className));
var tsComment = new TsComment(
Expand Down
183 changes: 165 additions & 18 deletions client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,41 +135,53 @@ static bool InstallTool()
return true;
}

var proc = new Process();
var installCommand = $"tool install Beamable.Tools --create-manifest-if-needed --allow-downgrade";

if (Application.isBatchMode)
// Only add the local folder feed for the local dev version (0.0.123.*). For a published
// version, installing from a local folder feed extracts into the global packages folder
// without copying the .nupkg, which then crashes finalization; let it resolve from nuget.org.
if (Application.isBatchMode && BeamableEnvironment.NugetPackageVersion.ToString().StartsWith("0.0.123"))
{
installCommand += " --add-source BeamableNugetSource ";
}

if (!BeamableEnvironment.NugetPackageVersion.ToString().Equals("0.0.123"))
// Lowercase the version: dotnet tool install builds the global-packages path from the raw --version string, so an uppercase pre-release label (7.2.0-PREVIEW.RC1) mismatches NuGet's lowercased cache folder on case-sensitive (Linux) filesystems.
var versionArg = BeamableEnvironment.NugetPackageVersion.ToString().ToLowerInvariant();
if (!versionArg.Equals("0.0.123"))
{
installCommand += $" --version {BeamableEnvironment.NugetPackageVersion}";
installCommand += $" --version {versionArg}";
}
else
{
installCommand += $" --version {BeamableEnvironment.NugetPackageVersion}.*";
installCommand += $" --version {versionArg}.*";
}

proc.StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
WorkingDirectory = Path.GetFullPath("."),
Arguments = installCommand,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
proc.StartInfo.Environment.Add("DOTNET_CLI_UI_LANGUAGE", "en");
var proc = NewInstallProcess();
Debug.Log($"[BeamCLI] Installing Beam CLI version [{BeamableEnvironment.NugetPackageVersion}]. " +
$"Command: [dotnet {installCommand}]. A first-time install with a cold NuGet cache can be slow while packages download.");
TryRunWithTimeout(1);

const string errorGuide =
"Please try installing manually by https://help.beamable.com/CLI-Latest/cli/guides/getting-started/#installing or contact Beamable for further support.";

var output = proc.StandardOutput.ReadToEnd();
var error = proc.StandardError.ReadToEnd();

// On some SDKs the local tool installer caches the package without its .nupkg and then crashes finalizing it. Only when we see that exact failure, pre-download the package via a normal restore (which keeps the .nupkg) and retry once, so unaffected SDKs never pay the cost.
var installLog = (output ?? string.Empty) + (error ?? string.Empty);
if (proc.ExitCode != 0 && installLog.Contains(".nupkg") &&
(installLog.Contains("Could not find file") || installLog.Contains("FileNotFoundException")))
{
Debug.LogWarning("[BeamCLI] Install failed with a missing .nupkg in the global packages cache (known local tool-install issue). Healing the cache and pre-downloading the package, then retrying once.");
HealCorruptGlobalPackagesEntry();
WarmGlobalPackagesCache();

proc = NewInstallProcess();
TryRunWithTimeout(1);
output = proc.StandardOutput.ReadToEnd();
error = proc.StandardError.ReadToEnd();
}

if (!string.IsNullOrWhiteSpace(error) || proc.ExitCode != 0)
{
StringBuilder message = new StringBuilder("Unable to install BeamCLI");
Expand All @@ -180,6 +192,7 @@ static bool InstallTool()

message.AppendLine($"Error: {error}");
message.Append(errorGuide);
Debug.Log("Failed Install Command: " + installCommand);
Debug.LogError(message.ToString());
var tryAgainButtonInfo = new OptionDialogWindow.ButtonInfo()
{
Expand Down Expand Up @@ -218,16 +231,55 @@ static bool InstallTool()

return proc.ExitCode == 0;

Process NewInstallProcess()
{
var p = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
WorkingDirectory = Path.GetFullPath("."),
Arguments = installCommand,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
p.StartInfo.Environment.Add("DOTNET_CLI_UI_LANGUAGE", "en");
return p;
}

bool TryRunWithTimeout(int currentTry)
{
var timeoutMs = 30 * 1000 * currentTry;
Debug.Log($"[BeamCLI] Install attempt {currentTry} starting (timeout {timeoutMs / 1000}s)...");
var stopwatch = Stopwatch.StartNew();
proc.Start();
if (proc.WaitForExit(10 * 1000 * currentTry))
if (proc.WaitForExit(timeoutMs))
{
Debug.Log($"[BeamCLI] Install attempt {currentTry} exited with code {proc.ExitCode} after {stopwatch.Elapsed.TotalSeconds:0.#}s.");
return true;
}

// Kill the still-running process before retrying; a concurrent retry corrupts the NuGet cache (a version folder left without its .nupkg), which then fails every later restore.
Debug.LogError(
"dotnet tool install command did not finish fast enough; timed out. Trying again with longer timeout");
$"[BeamCLI] Install attempt {currentTry} did not finish within {timeoutMs / 1000}s. " +
"Killing the dotnet process before retrying to avoid corrupting the NuGet cache.");
try
{
if (!proc.HasExited)
{
proc.Kill();
}
proc.WaitForExit();
Debug.Log($"[BeamCLI] Killed the timed-out dotnet process from attempt {currentTry}.");
}
catch (Exception killEx)
{
Debug.LogWarning($"[BeamCLI] Failed to kill the timed-out dotnet install process: {killEx.Message}");
}

const int maxRetries = 5;
if (currentTry > maxRetries)
{
Expand All @@ -245,9 +297,104 @@ bool TryRunWithTimeout(int currentTry)
return false;
}

Debug.Log($"[BeamCLI] Retrying install with a longer timeout (attempt {currentTry + 1}).");
return TryRunWithTimeout(++currentTry);

}

void WarmGlobalPackagesCache()
{
var version = BeamableEnvironment.NugetPackageVersion.ToString();
// The local dev flow installs 0.0.123.* from the local folder feed, which already
// lands the .nupkg in the cache, so there is nothing to warm (and it is not on nuget.org).
if (version.StartsWith("0.0.123"))
{
return;
}

try
{
var warmDir = Path.Combine(Path.GetFullPath("Library/BeamableEditor"), "cli-cache-warm");
Directory.CreateDirectory(warmDir);
var csprojPath = Path.Combine(warmDir, "warm.csproj");
File.WriteAllText(csprojPath,
"<Project Sdk=\"Microsoft.NET.Sdk\">\n" +
" <PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>\n" +
$" <ItemGroup><PackageDownload Include=\"Beamable.Tools\" Version=\"[{version}]\" /></ItemGroup>\n" +
"</Project>\n");

var warm = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
WorkingDirectory = warmDir,
Arguments = $"restore \"{csprojPath}\"",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
warm.StartInfo.Environment.Add("DOTNET_CLI_UI_LANGUAGE", "en");
Debug.Log($"[BeamCLI] Warming the NuGet cache with Beamable.Tools [{version}] via PackageDownload restore...");
warm.Start();
if (!warm.WaitForExit(120 * 1000))
{
try { if (!warm.HasExited) warm.Kill(); }
catch { /* ignore */ }
Debug.LogWarning("[BeamCLI] Cache-warm restore timed out; continuing to the install anyway.");
return;
}

var output = warm.StandardOutput.ReadToEnd();
var error = warm.StandardError.ReadToEnd();
Debug.Log($"[BeamCLI] Cache-warm restore exited with code {warm.ExitCode}. {output} {error}");
}
catch (Exception warmEx)
{
Debug.LogWarning($"[BeamCLI] Cache-warm failed (continuing to the install anyway): {warmEx.Message}");
}
}

void HealCorruptGlobalPackagesEntry()
{
try
{
var packageId = "beamable.tools";
var version = BeamableEnvironment.NugetPackageVersion.ToString().ToLowerInvariant();
var globalPackages = System.Environment.GetEnvironmentVariable("NUGET_PACKAGES");
if (string.IsNullOrEmpty(globalPackages))
{
var home = System.Environment.GetEnvironmentVariable("HOME");
if (string.IsNullOrEmpty(home))
{
home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
}
globalPackages = Path.Combine(home, ".nuget", "packages");
}

var packageDir = Path.Combine(globalPackages, packageId, version);
if (!Directory.Exists(packageDir))
{
Debug.Log($"[BeamCLI] No pre-existing global-packages entry at [{packageDir}].");
return;
}

var nupkgCount = Directory.GetFiles(packageDir, "*.nupkg").Length;
var totalFiles = Directory.GetFiles(packageDir, "*", SearchOption.AllDirectories).Length;
Debug.Log($"[BeamCLI] Found global-packages entry [{packageDir}]: {nupkgCount} .nupkg, {totalFiles} total files.");
if (nupkgCount == 0)
{
Debug.LogWarning($"[BeamCLI] Entry [{packageDir}] is missing its .nupkg (corrupt). Deleting it so the install can re-download a complete copy.");
Directory.Delete(packageDir, true);
}
}
catch (Exception healEx)
{
Debug.LogWarning($"[BeamCLI] Auto-heal of the global-packages cache failed: {healEx.Message}");
}
}
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions dev-web.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ echo " [cmd] pnpm version $VERSION --no-git-tag-version"
pnpm version "$VERSION" --no-git-tag-version
echo " [cmd] pnpm build"
pnpm build
echo " [cmd] pnpm publish --registry $REGISTRY --no-git-checks"
pnpm publish --registry "$REGISTRY" --no-git-checks
echo " [cmd] pnpm publish --registry $REGISTRY --no-git-checks --tag local"
pnpm publish --registry "$REGISTRY" --no-git-checks --tag local

cp package.json.devbak package.json && rm package.json.devbak
SDK_BACKUP=false
Expand Down Expand Up @@ -119,8 +119,8 @@ pnpm version "$VERSION" --no-git-tag-version

echo " [cmd] pnpm build"
pnpm build
echo " [cmd] pnpm publish --registry $REGISTRY --no-git-checks"
pnpm publish --registry "$REGISTRY" --no-git-checks
echo " [cmd] pnpm publish --registry $REGISTRY --no-git-checks --tag local"
pnpm publish --registry "$REGISTRY" --no-git-checks --tag local

cp package.json.devbak package.json && rm package.json.devbak
TOOLKIT_BACKUP=false
Expand Down
3 changes: 2 additions & 1 deletion template_updater/update-template.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.Text.RegularExpressions;

// ── Args ───────────────────────────────────────────────────────────────────────
var validTemplates = new[] { "BeamService", "BeamStorage", "PortalExtensionApp" };
var validTemplates = new[] { "BeamService", "BeamStorage", "PortalExtensionApp", "PortalExtensionReactApp" };

if (args.Length < 1) { Usage(); return; }

Expand Down Expand Up @@ -85,6 +85,7 @@
break;
}
case "PortalExtensionApp":
case "PortalExtensionReactApp":
{
foreach (string f in EnumerateFiles(searchPath, "package.json", ignorePaths, [templatesDir]))
{
Expand Down
Loading
Loading