spmcbride1201/keypunch-electron

View on GitHub
app/utils/jesFtp.js

Summary

Maintainability
F
4 days
Test Coverage
import Promise from 'bluebird';
import { store } from '../index';
import {
  setCurrentStep,
  setIsConnected,
  setIsConnecting,
  setIsSubmitted,
  setIsSubmitting,
  setIsRetrieved,
  setIsRetrieving,
  setIsDisconnected,
  setIsDisconnecting
} from '../actions/results';
import { refreshJobs, loadJobResults } from '../actions/jobs';
import { setExplorerContent } from '../actions/explorer';
import { refreshDatasets } from '../actions/datasets';

const PromiseFtp = require('promise-ftp');

class JES {
  constructor() {
    this.ftp = new PromiseFtp();
    this.connectionStatus = '';
    //bind all the things here
    this.connect = this.connect.bind(this);
    this.disconnect = this.disconnect.bind(this);
    this.setFiletype = this.setFiletype.bind(this);
    this.setEncoding = this.setEncoding.bind(this);
    this.pollJobStatus = this.pollJobStatus.bind(this);
    this.submitJob = this.submitJob.bind(this);
    this.deleteJob = this.deleteJob.bind(this);
    this.listDatasets = this.listDatasets.bind(this);
    this.listMembers = this.listMembers.bind(this);
    this.retrieveMember = this.retrieveMember.bind(this);
    this._errorLookup = this._errorLookup.bind(this);
    this._sleep = this._sleep.bind(this);
  }

  // Connect if not already connected
  connect() {
    if (this.ftp.getConnectionStatus() !== 'connected') {
      store.dispatch(setIsConnecting(true));
      store.dispatch(setIsConnected(false));
      return this.ftp.connect({
        host: store.getState().config.hostName,
        port: store.getState().config.ftpPort,
        user: store.getState().config.ftpUserName,
        password: store.getState().config.ftpPassword
      })
        .then((msg) => console.log(msg))
        .then(() => {
          if (this.ftp.getConnectionStatus() === 'connected') {
            store.dispatch(setIsConnecting(false));
            store.dispatch(setIsConnected(true));
          }
        })
        .catch(err => this._errorLookup(err))
    } else {
      //return promise that resolves immediately
      return Promise.resolve("Already connected");
    }
  }

  async disconnect() {
    this.connectionStatus = this.ftp.getConnectionStatus();
    console.log("Attempting to disconnect");
    this.ftp.end()
      .then((msg) => console.log(msg))
      .catch(err => _errorLookup(err))
    // .then(async () => {
    this.connectionStatus = this.ftp.getConnectionStatus();
    console.log("Connection Status: ", this.connectionStatus);
    if (this.connectionStatus === 'disconnecting') {
      store.dispatch(setIsDisconnecting(true));
    }
    let numTries = 10;
    while (this.connectionStatus === 'disconnecting' && numTries > 0) {
      await this._sleep(2000);
      this.connectionStatus = this.ftp.getConnectionStatus();
      console.log("Connection Status: ", this.connectionStatus);
      numTries--
    }
    if (numTries == 0) {
      console.log("Failed to gracefully disconnect from the server, so forcing destroy");
      this.ftp.destroy()
      this.connectionStatus = this.ftp.getConnectionStatus();
      console.log("Connection Status: ", this.connectionStatus);
    }
    // Clear all indicators
    store.dispatch(setIsConnected(false));
    store.dispatch(setIsConnecting(false));
    store.dispatch(setIsSubmitted(false));
    store.dispatch(setIsSubmitting(false));
    store.dispatch(setIsRetrieved(false));
    store.dispatch(setIsRetrieving(false));
    store.dispatch(setIsDisconnected(false));
    store.dispatch(setIsDisconnecting(false));
  }

  setFiletype(filetype) {
    let acceptableFileTypes = ['jes', 'seq'];
    if (acceptableFileTypes.includes(filetype)) {
      // if (store.getState().isConnected === false) this.connect();
      return this.ftp.site('FILETYPE=' + filetype)
        .then((resultsObj) => {
          console.log("Server responded to FILETYPE=jes with " + resultsObj.code + ' ' + resultsObj.text);
        })
        .catch(err => this._errorLookup(err))
    }
  }

  setEncoding(type) {
    let acceptableFileTypes = ['ascii'];
    if (acceptableFileTypes.includes(type)) {
      switch (type) {
        case 'ascii':
          return this.ftp.ascii()
            .then(res => {
              console.log(res);
              console.log(JSON.stringify(res));
            })
          break;
        default:
          return
      }
    }
  }

  // Get all JES jobs and return as an array of objects;

  pollJobStatus() {
    store.dispatch(setIsSubmitting(true));
    store.dispatch(setIsRetrieving(true));
    this.connect()
      .then(() => this.setEncoding('ascii'))
      .then(() => this.setFiletype('jes'))
      .then(() => this.ftp.list(''))
      .then(results => {
        console.log('Checking results of list: ', results);
        // Because the mainframe returns an information message if the queue is empty,
        // this is an error
        if (!results || results.length === 0) {
          throw new Error("Unable to retrieve jobs from the mainframe JES Queue.");
        }
        // The mainframe will provide an informational string back if no jobs are found.
        // We need to explicitly check for this and any other such informational messages.
        else if (results[0] === "No jobs found on Held queue") {
          store.dispatch(refreshJobs({}));
          store.dispatch(setIsSubmitting(false));
          store.dispatch(setIsRetrieving(false));
        } else {
          let jobs = {}
          results.forEach((job) => {
            let jobSplit = job.trim().split(/\ +/);
            let jobID = jobSplit[1];
            jobs[jobID] = {
              owner: jobSplit[0],
              status: jobSplit[2],
              numberOfSpoolFiles: job.includes('Spool Files') ? jobSplit[3] : null,
              jobID: jobID,
              fullString: job.trim()
            };
          });
          store.dispatch(refreshJobs(jobs));
          store.dispatch(setIsSubmitting(false));
          store.dispatch(setIsRetrieving(false));
        }
      })
      .catch(err => this._errorLookup(err));
  }

  deleteJob(jobID) {
    store.dispatch(setIsSubmitting(true));
    store.dispatch(setIsRetrieving(true));
    return this.connect()
      .then((msg) => this.setEncoding('ascii'))
      .then((msg) => console.log('After setting encoding to ASCII: ', msg))
      .then((msg) => this.setFiletype('jes'))
      .then((msg) => console.log('After setting filetype to JES: ', msg))
      .then((msg) => this.ftp.delete(jobID))
      .then((res) => {
        store.dispatch(setIsSubmitting(false));
        store.dispatch(setIsRetrieving(false));
        console.log(`After invoking delete on ${jobID}: ${res}`)
      })
      .catch(err => this._errorLookup(err));
  }

  // TODO: Put returns an empty array, but we expect a string output containing job
  // control data such as the job ID. This is probably not getting handled properly
  // by the underlying promise-ftp library, but it's possible that I'm not using
  // the correct command

  // A job can be a buffer, a file, and others????
  // Buffer.from(store.getState().editor.editorContent)
  // TODO: Check and ensure JES mode and ASCII

  submitJob(job) {
    store.dispatch(setIsSubmitting(true));
    store.dispatch(setIsRetrieving(true));
    return this.connect()
      .then((msg) => this.setEncoding('ascii'))
      .then((msg) => console.log('After setting encoding to ASCII: ', msg))
      .then((msg) => this.setFiletype('jes'))
      .then((msg) => console.log('After setting filetype to JES: ', msg))
      .then((msg) => this.ftp.put(job, '/'))
      .then((res) => {
        store.dispatch(setIsSubmitting(false));
        store.dispatch(setIsRetrieving(false));
        console.log(`After invoking put on ${job}: ${res}`)
      })
      .catch(err => this._errorLookup(err));
  }

  //The x suffix seems to be needed to retrieve a printout of the jobs output;
  retrieveJob(jobID) {
    store.dispatch(setIsSubmitting(true));
    store.dispatch(setIsRetrieving(true));
    return this.connect()
      .then((msg) => this.setEncoding('ascii'))
      .then((msg) => console.log('After setting encoding to ASCII: ', msg))
      .then((msg) => this.setFiletype('jes'))
      .then((msg) => console.log('After setting filetype to JES: ', msg))
      .then((msg) => this.ftp.get(jobID + '.x'))
      .then((stream) => {
        console.log(`After invoking get on ${jobID}:`)
        // stream.once('close', resolve);
        // stream.once('error', reject);
        let results = '';
        stream.on('data', (chunk) => {
          console.log(`Received ${results.length} bytes of data.`);
          results += chunk.toString();
        })
        stream.on('end', () => {
          console.log('final output:\n' + results)
          store.dispatch(loadJobResults(jobID, results))
          store.dispatch(setIsSubmitting(false));
          store.dispatch(setIsRetrieving(false));
        })
      })
      .catch(err => this._errorLookup(err));
  }

  listDatasets() {
    store.dispatch(setIsSubmitting(true));
    store.dispatch(setIsRetrieving(true));
    let datasets = [];
    return this.connect()
      .then(() => this.setEncoding('ascii'))
      .then(() => this.setFiletype('seq'))
      .then(() => this.ftp.list(''))
      .then(results => {
        console.log('Checking results of list: ', results);
        if (!results || results.length === 0) {
          throw new Error("Unable to list datasets.");
        }
        results.shift(); // Filter out the first row, which is table headers
        let highLevelQualifier = store.getState().config.ftpUserName + '.';
        results.forEach((dataset) => {
          const datasetSplit = dataset.trim().split(/\ +/);
          const volume = datasetSplit[0];
          const unit = datasetSplit[1];
          const referred = datasetSplit[2];
          const ext = datasetSplit[3];
          const used = datasetSplit[4];
          const recfm = datasetSplit[5];
          const lrecl = datasetSplit[6];
          const blksz = datasetSplit[7];
          const dsorg = datasetSplit[8];
          // let dsname = highLevelQualifier + datasetSplit[9];
          const dsname = datasetSplit[9];
          datasets.push({
            name: dsname,
            toggled: false,
            children: [],
            attributes: {
              volume,
              unit,
              referred,
              ext,
              used,
              recfm,
              lrecl,
              blksz,
              dsorg,
              dsname
            }
          });
        });
        console.log(datasets);
      })
      .then(() => {
        return Promise.each(datasets, (dataset) => {
          return this.listMembers(dataset.attributes.dsname, datasets)
            .then(_datasets => {
              console.log("_datasets is: ", _datasets);
              datasets = _datasets
            })
        })
      })
      .then(() => {
        store.dispatch(refreshDatasets(datasets));
        store.dispatch(setIsSubmitting(false));
        store.dispatch(setIsRetrieving(false));
      })
      .catch(err => this._errorLookup(err));
  }

  // This actually populates the members in each dataset
  listMembers(dsname, datasets) {
    store.dispatch(setIsSubmitting(true));
    store.dispatch(setIsRetrieving(true));
    console.log("Connection: ", this.ftp.getConnectionStatus());
    let pwd;
    console.log(`list the members for ${dsname}`);
    return this.connect()
      .then((msg) => this.setEncoding('ascii'))
      .then((msg) => this.setFiletype('seq'))
      .then((msg) => this.ftp.pwd())
      .then((msg) => console.log('After pwd: ', msg))
      .then(() => this.ftp.cwd(dsname))
      .then((msg) => console.log('After cwd: ', msg))
      .then(() => this.ftp.list(''))
      .then((res) => {
        console.log(res);
        res.shift(); //
        return res.forEach(member => {
          let memberSplit = member.trim().split(/\ +/);
          let name = memberSplit[0] || '';
          let vvmm = memberSplit[1] || '';
          let created = memberSplit[2] || '';
          let changed = memberSplit[3] || '';
          let size = memberSplit[4] || '';
          let init = memberSplit[5] || '';
          let mod = memberSplit[6] || '';
          let id = memberSplit[7] || '';
          let dsorg = memberSplit[8] || '';
          console.log({ name, vvmm, created, changed, size, init, mod, id, dsorg });
          console.log(dsname);
          let index = datasets.findIndex((dataset) => { return dataset.attributes.dsname == dsname });
          console.log("index is: ", index);
          datasets[index].children.push({
            name: name,
            attributes: {
              name: name,
              vvmm: vvmm,
              created: created,
              changed: changed,
              size: size,
              init: init,
              mod: mod,
              id: id,
              dsorg: dsorg,
              dsname: dsname
            }
          })
        })
      })
      .catch(err => console.log(err)) // "Error: No Members Found is returned if nothing is found"
      // MVS treats the home directory as the high-level-qualifier set to the z/OS userid
      .then(() => this.ftp.cwd('~'))
      .then((msg) => {
        console.log('After cwd: ', msg)
        store.dispatch(setIsSubmitting(false));
        store.dispatch(setIsRetrieving(false));
      })
      .catch(err => this._errorLookup(err))
      .then(() => datasets)
  }

  retrieveMember(datasetName, memberName) {
    store.dispatch(setIsSubmitting(true));
    store.dispatch(setIsRetrieving(true));
    console.log(`Retrieving member ${memberName} from dataset ${datasetName}`);
    return this.connect()
      .then(() => this.setEncoding('ascii'))
      .then((msg) => console.log('After setting encoding to ASCII: ', msg))
      .then(() => this.setFiletype('seq'))
      .then(() => this.ftp.cwd('~'))
      .then((msg) => console.log('After cwd: ', msg))
      .then(() => this.ftp.cwd(datasetName))
      .then((msg) => console.log('After cwd: ', msg))
      .then(() => this.ftp.get(memberName))
      .then((stream) => {
        console.log(`After invoking get on ${memberName}:`)
        let results = '';
        stream.on('data', (chunk) => {
          console.log(`Received ${results.length} bytes of data.`);
          results += chunk.toString();
        })
        stream.on('end', () => {
          console.log('final output:\n' + results);
          store.dispatch(setExplorerContent(results));
          store.dispatch(setIsSubmitting(false));
          store.dispatch(setIsRetrieving(false));
        })
      })
      .catch(err => this._errorLookup(err))
      .finally(() => this.ftp.cwd('~'))
  }

  // Private Helper Functions

  _errorLookup(err) {
    store.dispatch(setIsSubmitting(false));
    store.dispatch(setIsRetrieving(false));
    console.log("BUG DETECTED");
    console.log("Connection: ", this.ftp.getConnectionStatus());
    if (err.code) {
      switch (err.code) {
        case 451:
          // alert("File Error. Do you have a valid file open in the editor?");
          console.log(err);
          break;
        case 550:
          // alert("Permission Denied (or No such file or folder):\n", err.toString());
          console.log(err);
          break;
        case "Error: PASS command failed(…)":
          // alert("What is a PASS command?");
          break;
        default:
          console.log("|", err, "|");
        // console.log(err.parse(':'));
        // alert(JSON.stringify(err));
      }
    } else {
      console.log(err.toString());
      console.log("Just in Case: ", JSON.stringify(err));
    }
  }

  //--------------------------------------------------------------------------------------------------
  // _pollMostRecentJobUntilComplete
  //     - maxRetries: The Number of times to poll job status before giving up
  //     - timeToWait: The amount of time in ms that we should wait between retries
  //
  //     - RETURNS mostRecentJob
  //
  // Repeatedly poll job status until the most recent job has a status of output, signifying that
  // batch processing is complete. When complete, return the status of the most recent job to
  // allow for follow on action.
  //
  // TODO: Each iteration of the loop recalculates what the mostRecentJob is. This needs to target
  // the same JES Job ID.
  //
  // TODO: Assuming escalated privileges on the current User ID, it is possible that the output of
  // _pollJobStatus returns other user's jobs. Based on the workflow of this function, we should
  // really treat the most recent job as the most recent job submitted by the user. Perhaps this
  // should be handled in _pollJobStatus via an optional flag.
  //
  //--------------------------------------------------------------------------------------------------

  async _pollMostRecentJobUntilComplete(maxRetries, timeToWait) {
    console.log("Attempting to get most recent job");
    let mostRecentJob;
    while (!mostRecentJob || mostRecentJob.status !== 'OUTPUT' && maxRetries > 0) {
      console.log("TriesRemaining: ", maxRetries);
      let jobs = await this.pollJobStatus();
      // If we don't know the most recent job, we guestimate based on the job with the highest JOB ID
      // This should only run once, as it's possible that we submit another job while this one is processing
      if (!mostRecentJob) {
        const compareJobIDs = (firstJobID, secondJobID) => {
          let firstNum = parseInt(firstJobID.substring(3));
          let secondNum = parseInt(secondJobID.substring(3));
          if (firstNum > secondNum) return 1;
          if (firstNum < secondNum) return -1;
          // Equality shouldn't be possible, but included just to prevent blow-ups
          if (firstNum === secondNum) return 0;
        }
        jobIDs = Object.keys(jobs).sort(compareJobIDs);
        mostRecentJobID = jobIDs[jobIDs.length - 1];
        mostRecentJob = jobs(mostRecentJobID);
      }
      // If we're already guestimated what the most recent job is, just refresh it from the output
      else {
        mostRecentJob = jobs(mostRecentJob.jobID);
      }
      if (mostRecentJob.status === 'OUTPUT') {
        console.log("Most recent job has status of OUTPUT, so returning");
        return mostRecentJob;
      }
      sleep(timeToWait)
      maxRetries--;
    }
    return mostRecentJob;
  }

  _sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

const jes = new JES();
export default jes;

// accepts 'jes'