Skip to content

Commit

Permalink
Enable Hybrid execution in Electron (#12408)
Browse files Browse the repository at this point in the history
related: #12198

Changelog:
- update: enable `Run locally` menu option in Electron app
- update: open Hybrid execution for internal testing

# Important Notes
https://github.com/user-attachments/assets/d449d35c-97ec-459e-ac1e-60ba9306bf10
  • Loading branch information
4e6 authored Mar 4, 2025
1 parent 27c6459 commit 4d1cdfe
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 0 deletions.
1 change: 1 addition & 0 deletions app/gui/src/dashboard/providers/FeatureFlagsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function featureFlagsForInternalTesting() {
enableCloudExecution: true,
enableAsyncExecution: true,
enableAdvancedProjectExecutionOptions: false,
enableHybridExecution: true,
}
}

Expand Down
34 changes: 34 additions & 0 deletions app/ide-desktop/client/src/projectManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,40 @@ export function isProjectInstalled(
return pathModule.resolve(projectRootParent) === pathModule.resolve(directory)
}

/** Create a .tar.gz enso-project bundle. */
export function createBundle(directory: string): Promise<Buffer> {
const readableStream = tar.c(
{
z: true,
C: directory,
},
['.'],
)
return new Promise((resolve, reject) => {
const chunks: Buffer[] = []
readableStream.on('data', (data) => chunks.push(data))
readableStream.on('end', () => resolve(Buffer.concat(chunks)))
readableStream.on('error', reject)
})
}

/** Unpack a .tar.gz enso-project bundle into a temporary directory */
export async function unpackBundle(
bundle: stream.Readable,
targetDirectory: string,
): Promise<string> {
return new Promise((resolve, reject) => {
bundle
.pipe(
tar.x({
C: targetDirectory,
}),
)
.on('end', () => resolve(targetDirectory))
.on('error', (err) => reject(err))
})
}

// ==================
// === Project ID ===
// ==================
Expand Down
102 changes: 102 additions & 0 deletions app/ide-desktop/client/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as mkcert from 'mkcert'
import * as fs from 'node:fs/promises'
import * as http from 'node:http'
import * as https from 'node:https'
import * as path from 'node:path'
import * as stream from 'node:stream'

Expand Down Expand Up @@ -34,6 +35,7 @@ ydocServer.configureAllDebugLogs(
const HTTP_STATUS_OK = 200
const HTTP_STATUS_BAD_REQUEST = 400
const HTTP_STATUS_NOT_FOUND = 404
const HTTP_STATUS_INTERNAL_SERVER_ERROR = 500

// ==============
// === Config ===
Expand Down Expand Up @@ -203,6 +205,106 @@ export class Server {
),
{ end: true },
)
} else if (requestUrl.startsWith('/api/cloud/')) {
switch (requestPath) {
case '/api/cloud/download-project': {
const url = new URL(`https://example.com/${requestUrl}`)
const downloadUrl = url.searchParams.get('downloadUrl')
const projectId = url.searchParams.get('projectId')

if (downloadUrl == null) {
response
.writeHead(HTTP_STATUS_BAD_REQUEST, COOP_COEP_CORP_HEADERS)
.end('Request is missing search parameter `downloadUrl`.')
break
}

if (projectId == null) {
response
.writeHead(HTTP_STATUS_BAD_REQUEST, COOP_COEP_CORP_HEADERS)
.end('Request is missing search parameter `projectId`.')
break
}

https.get(downloadUrl, (actualResponse) => {
const projectsDirectory = projectManagement.getProjectsDirectory()
const targetDirectory = path.join(projectsDirectory, `cloud-${projectId}`)

fs.mkdir(targetDirectory, { recursive: true })
.then(() => projectManagement.unpackBundle(actualResponse, targetDirectory))
.then((projectDirectory) => {
response.writeHead(HTTP_STATUS_OK, COOP_COEP_CORP_HEADERS).end(projectDirectory)
})
.catch((e) => {
console.error(e)
response.writeHead(HTTP_STATUS_INTERNAL_SERVER_ERROR, COOP_COEP_CORP_HEADERS).end()
})
})

break
}
case '/api/cloud/upload-project': {
const url = new URL(`https://example.com/${requestUrl}`)
const uploadUrl = url.searchParams.get('uploadUrl')
const projectDir = url.searchParams.get('directory')

if (uploadUrl == null) {
response
.writeHead(HTTP_STATUS_BAD_REQUEST, COOP_COEP_CORP_HEADERS)
.end('Request is missing search parameter `uploadUrl`.')
break
}
if (projectDir == null) {
response
.writeHead(HTTP_STATUS_BAD_REQUEST, COOP_COEP_CORP_HEADERS)
.end('Request is missing search parameter `directory`.')
break
}

projectManagement
.createBundle(projectDir)
.then((projectBundle) => {
const headers = {
authorization: request.headers.authorization,
}
const uploadRequest = https.request(
uploadUrl,
{ method: 'POST', headers },
(actualResponse) => {
if (!response.writableFinished) {
response.writeHead(
// This is SAFE. The documentation says:
// Only valid for response obtained from ClientRequest.
actualResponse.statusCode!,
actualResponse.statusMessage,
actualResponse.headers,
)
actualResponse.pipe(response, { end: true })
}
},
)
uploadRequest.write(projectBundle, (err) => {
if (err) {
console.error(err)
response
.writeHead(HTTP_STATUS_INTERNAL_SERVER_ERROR)
.end('Failed to write project bundle.')
}
})
uploadRequest.end()
})
.catch((err) => {
console.error(err)
response.writeHead(HTTP_STATUS_INTERNAL_SERVER_ERROR, COOP_COEP_CORP_HEADERS).end()
})

break
}
default: {
console.error(`Unknown Cloud middleware request:`, requestPath)
break
}
}
} else if (request.method === 'POST') {
switch (requestPath) {
case '/api/upload-file': {
Expand Down

0 comments on commit 4d1cdfe

Please sign in to comment.