• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2018 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {produce} from 'immer';
16
17import {assertExists} from '../base/logging';
18import {Actions} from '../common/actions';
19import {ConversionJobStatus} from '../common/conversion_jobs';
20import {
21  createEmptyNonSerializableState,
22  createEmptyState,
23} from '../common/empty_state';
24import {EngineConfig, ObjectById, State} from '../common/state';
25import {STATE_VERSION} from '../common/state';
26import {
27  BUCKET_NAME,
28  buggyToSha256,
29  deserializeStateObject,
30  saveState,
31  saveTrace,
32  toSha256,
33} from '../common/upload_utils';
34import {globals} from '../frontend/globals';
35import {publishConversionJobStatusUpdate} from '../frontend/publish';
36import {Router} from '../frontend/router';
37
38import {Controller} from './controller';
39import {RecordConfig, recordConfigValidator} from './record_config_types';
40import {runValidator} from './validators';
41
42interface MultiEngineState {
43  currentEngineId?: string;
44  engines: ObjectById<EngineConfig>
45}
46
47function isMultiEngineState(state: State|
48                            MultiEngineState): state is MultiEngineState {
49  if ((state as MultiEngineState).engines !== undefined) {
50    return true;
51  }
52  return false;
53}
54
55export class PermalinkController extends Controller<'main'> {
56  private lastRequestId?: string;
57  constructor() {
58    super('main');
59  }
60
61  run() {
62    if (globals.state.permalink.requestId === undefined ||
63        globals.state.permalink.requestId === this.lastRequestId) {
64      return;
65    }
66    const requestId = assertExists(globals.state.permalink.requestId);
67    this.lastRequestId = requestId;
68
69    // if the |hash| is not set, this is a request to create a permalink.
70    if (globals.state.permalink.hash === undefined) {
71      const isRecordingConfig =
72          assertExists(globals.state.permalink.isRecordingConfig);
73
74      const jobName = 'create_permalink';
75      publishConversionJobStatusUpdate({
76        jobName,
77        jobStatus: ConversionJobStatus.InProgress,
78      });
79
80      PermalinkController.createPermalink(isRecordingConfig)
81          .then((hash) => {
82            globals.dispatch(Actions.setPermalink({requestId, hash}));
83          })
84          .finally(() => {
85            publishConversionJobStatusUpdate({
86              jobName,
87              jobStatus: ConversionJobStatus.NotRunning,
88            });
89          });
90      return;
91    }
92
93    // Otherwise, this is a request to load the permalink.
94    PermalinkController.loadState(globals.state.permalink.hash)
95        .then((stateOrConfig) => {
96          if (PermalinkController.isRecordConfig(stateOrConfig)) {
97            // This permalink state only contains a RecordConfig. Show the
98            // recording page with the config, but keep other state as-is.
99            const validConfig =
100                runValidator(recordConfigValidator, stateOrConfig as unknown)
101                    .result;
102            globals.dispatch(Actions.setRecordConfig({config: validConfig}));
103            Router.navigate('#!/record');
104            return;
105          }
106          globals.dispatch(Actions.setState({newState: stateOrConfig}));
107          this.lastRequestId = stateOrConfig.permalink.requestId;
108        });
109  }
110
111  private static upgradeState(state: State): State {
112    if (state.version !== STATE_VERSION) {
113      const newState = createEmptyState();
114      // Old permalinks from state versions prior to version 24
115      // have multiple engines of which only one is identified as the
116      // current engine via currentEngineId. Handle this case:
117      if (isMultiEngineState(state)) {
118        const engineId = state.currentEngineId;
119        if (engineId !== undefined) {
120          newState.engine = state.engines[engineId];
121        }
122      } else {
123        newState.engine = state.engine;
124      }
125
126      if (newState.engine !== undefined) {
127        newState.engine.ready = false;
128      }
129
130      const message = `Unable to parse old state version. Discarding state ` +
131          `and loading trace.`;
132      console.warn(message);
133      PermalinkController.updateStatus(message);
134      return newState;
135    } else {
136      // Loaded state is presumed to be compatible with the State type
137      // definition in the app. However, a non-serializable part has to be
138      // recreated.
139      state.nonSerializableState = createEmptyNonSerializableState();
140    }
141    return state;
142  }
143
144  private static isRecordConfig(stateOrConfig: State|
145                                RecordConfig): stateOrConfig is RecordConfig {
146    const mode = (stateOrConfig as {mode?: string}).mode;
147    return mode !== undefined &&
148        ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'].includes(mode);
149  }
150
151  private static async createPermalink(isRecordingConfig: boolean):
152      Promise<string> {
153    let uploadState: State|RecordConfig = globals.state;
154
155    if (isRecordingConfig) {
156      uploadState = globals.state.recordConfig;
157    } else {
158      const engine = assertExists(globals.getCurrentEngine());
159      let dataToUpload: File|ArrayBuffer|undefined = undefined;
160      let traceName = `trace ${engine.id}`;
161      if (engine.source.type === 'FILE') {
162        dataToUpload = engine.source.file;
163        traceName = dataToUpload.name;
164      } else if (engine.source.type === 'ARRAY_BUFFER') {
165        dataToUpload = engine.source.buffer;
166      } else if (engine.source.type !== 'URL') {
167        throw new Error(`Cannot share trace ${JSON.stringify(engine.source)}`);
168      }
169
170      if (dataToUpload !== undefined) {
171        PermalinkController.updateStatus(`Uploading ${traceName}`);
172        const url = await saveTrace(dataToUpload);
173        // Convert state to use URLs and remove permalink.
174        uploadState = produce(globals.state, (draft) => {
175          assertExists(draft.engine).source = {type: 'URL', url};
176          draft.permalink = {};
177        });
178      }
179    }
180
181    // Upload state.
182    PermalinkController.updateStatus(`Creating permalink...`);
183    const hash = await saveState(uploadState);
184    PermalinkController.updateStatus(`Permalink ready`);
185    return hash;
186  }
187
188  private static async loadState(id: string): Promise<State|RecordConfig> {
189    const url = `https://storage.googleapis.com/${BUCKET_NAME}/${id}`;
190    const response = await fetch(url);
191    if (!response.ok) {
192      throw new Error(
193          `Could not fetch permalink.\n` +
194          `Are you sure the id (${id}) is correct?\n` +
195          `URL: ${url}`);
196    }
197    const text = await response.text();
198    const stateHash = await toSha256(text);
199    const state = deserializeStateObject(text);
200    if (stateHash !== id) {
201      // Old permalinks incorrectly dropped some digits from the
202      // hexdigest of the SHA256. We don't want to invalidate those
203      // links so we also compute the old string and try that here
204      // also.
205      const buggyStateHash = await buggyToSha256(text);
206      if (buggyStateHash !== id) {
207        throw new Error(`State hash does not match ${id} vs. ${stateHash}`);
208      }
209    }
210    if (!this.isRecordConfig(state)) {
211      return this.upgradeState(state);
212    }
213    return state;
214  }
215
216  private static updateStatus(msg: string): void {
217    // TODO(hjd): Unify loading updates.
218    globals.dispatch(Actions.updateStatus({
219      msg,
220      timestamp: Date.now() / 1000,
221    }));
222  }
223}
224