import { BaseProject , CopyOptions , ProjectStatus , ProjectState , getProject , } from "./base" ; import { db } from "@cocalc/database" ; import { callback2 } from "@cocalc/util/async-utils" ; import { is_valid_uuid_string , uuid } from "@cocalc/util/misc" ; import getLogger from "@cocalc/backend/logger" ; const winston = getLogger ( "project-control-kucalc" ) ; class Project extends BaseProject { constructor ( project_id ) { super ( project_id ) ; } private async get ( columns ) { return await callback2 ( db ( ) . get_project , { project_id : this . project_id , columns , } ) ; } async state ( ) { return ( await this . get ( [ "state" ] ) ) ?. state ?? { } ; } async status ( ) { const status = ( await this . get ( [ "status" ] ) ) ?. status ?? { } ; status [ "hub-server.port" ] = 6000 ; status [ "browser-server.port" ] = 6001 ; return status ; } async start ( ) { if ( this . stateChanging != null ) return ; winston . info ( `start ${ this . project_id }` ) ; if ( ( await this . state ( ) ) . state == "running" ) { winston . debug ( "start -- already running" ) ; return ; } try { this . stateChanging = { state : "starting" } ; await this . siteLicenseHook ( ) ; await this . actionRequest ( "start" ) ; await this . waitUntilProject ( ( project ) => project . state ?. state == "running" || project . action_request ?. finished , 120 ) ; } finally { this . stateChanging = undefined ; } } async stop ( ) { if ( this . stateChanging != null ) return ; winston . info ( "stop " , this . project_id ) ; if ( ( await this . state ( ) ) . state != "running" ) { return ; } try { this . stateChanging = { state : "stopping" } ; await this . actionRequest ( "stop" ) ; await this . waitUntilProject ( ( project ) => ( project . state != null && project . state != "running" && project . state != "stopping" ) || project . action_request ?. finished , 60 ) ; } finally { this . stateChanging = undefined ; } } async copyPath ( opts ) { const dbg = this . dbg ( "copyPath" ) ; dbg ( JSON . stringify ( opts ) ) ; if ( opts . path == null ) { throw Error ( "path must be specified" ) ; } opts . target_project_id = opts . target_project_id ? opts . target_project_id : this . project_id ; opts . target_path = opts . target_path ? opts . target_path : opts . path ; if ( ! is_valid_uuid_string ( opts . target_project_id ) ) { throw Error ( `target_project_id=${ opts . target_project_id } is invalid` ) ; } const copyID = uuid ( ) ; dbg ( `copyID=${ copyID }` ) ; if ( opts . scheduled && opts . scheduled instanceof Date ) { const d = new Date ( opts . scheduled ) ; const offset = d . getTimezoneOffset ( ) / 60 ; opts . scheduled = new Date ( d . getTime ( ) - offset ) ; opts . wait_until_done = false ; dbg ( `opts.scheduled = ${ opts . scheduled }` ) ; } dbg ( "write query requesting the copy to happen to the database" ) ; await callback2 ( db ( ) . _query , { query : "INSERT INTO copy_paths" , values : { "id                ::UUID" : copyID , "time              ::TIMESTAMP" : new Date ( ) , "source_project_id ::UUID" : this . project_id , "source_path       ::TEXT" : opts . path , "target_project_id ::UUID" : opts . target_project_id , "target_path       ::TEXT" : opts . target_path , "overwrite_newer   ::BOOLEAN" : opts . overwrite_newer , "public            ::BOOLEAN" : opts . public , "delete_missing    ::BOOLEAN" : opts . delete_missing , "backup            ::BOOLEAN" : opts . backup , "bwlimit           ::TEXT" : opts . bwlimit , "timeout           ::NUMERIC" : opts . timeout , "scheduled         ::TIMESTAMP" : opts . scheduled , "exclude           ::TEXT[]" : opts . exclude , } , } ) ; if ( opts . wait_until_done == true ) { dbg ( "waiting for the copy request to complete..." ) ; await this . waitUntilCopyFinished ( copyID , 60 * 4 ) ; dbg ( "finished" ) ; return "" ; } else { dbg ( "NOT waiting for copy to complete" ) ; return copyID ; } } private getProjectSynctable ( ) { return db ( ) . synctable ( { table : "projects" , columns : [ "state" , "action_request" ] , where : { "project_id = $::UUID" : this . project_id } , where_function : ( project_id ) => project_id == this . project_id , } ) ; } private async actionRequest ( action ) { await callback2 ( db ( ) . _query , { query : "UPDATE projects" , where : { "project_id  = $::UUID" : this . project_id } , jsonb_set : { action_request : { action , time : new Date ( ) , started : undefined , finished : undefined , } , } , } ) ; } private async waitUntilProject ( until , timeout ) { let synctable = undefined ; try { synctable = this . getProjectSynctable ( ) ; await callback2 ( synctable . wait , { until : ( ) => until ( synctable . get ( this . project_id ) ?. toJS ( ) ?? { } ) , timeout , } ) ; } finally { synctable ?. close ( ) ; } } private getCopySynctable ( copyID ) { return db ( ) . synctable ( { table : "copy_paths" , columns : [ "started" , "error" , "finished" ] , where : { "id = $::UUID" : copyID } , where_function : ( id ) => id == copyID , } ) ; } private async waitUntilCopyFinished ( copyID , timeout ) { let synctable = undefined ; try { synctable = this . getCopySynctable ( copyID ) ; await callback2 ( synctable . wait , { until : ( ) => synctable . getIn ( [ copyID , "finished" ] ) , timeout , } ) ; const err = synctable . getIn ( [ copyID , "error" ] ) ; if ( err ) { throw Error ( err ) ; } } finally { synctable ?. close ( ) ; } } } export default function get ( project_id ) { return ( getProject ( project_id ) as Project ) ?? new Project ( project_id ) ; }